1
0
Fork 0

Merge remote-tracking branch 'upstream/master' into feature/allow-configurable-file-extension-for-indexing

pull/668/head
Jürgen Steitz 2018-08-29 21:21:37 +02:00
commit 9cc2736df2
258 changed files with 16392 additions and 2109 deletions

View File

@ -7,3 +7,4 @@ fixtures/
coverage/ coverage/
coverage.xml coverage.xml
images/ images/
node_modules/

View File

@ -7,7 +7,7 @@ trim_trailing_whitespace = true
indent_style = space indent_style = space
indent_size = 4 indent_size = 4
[*.json,*.yml] [*.{json,yml}]
indent_size = 2 indent_size = 2
[composer.json] [composer.json]

23
.gitattributes vendored Normal file
View File

@ -0,0 +1,23 @@
* text=auto
/.vscode export-ignore
/fixtures export-ignore
/images export-ignore
/validation export-ignore
/.dockerignore export-ignore
/.editorconfig export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/.gitmodules export-ignore
/.npmrc export-ignore
/.travis.yml export-ignore
/appveyor.yml export-ignore
/codecov.yml export-ignore
/dependencies.yml export-ignore
/Dockerfile export-ignore
/package.json export-ignore
/Performance.php export-ignore
/php.ini export-ignore
/phpcs.xml.dist export-ignore
/phpunit.xml.dist export-ignore
/release-docker.php export-ignore

3
.gitignore vendored
View File

@ -1,7 +1,8 @@
.DS_Store .DS_Store
.vscode
.idea .idea
vendor/ vendor/
.phpls/ .phpls/
composer.lock composer.lock
stubs stubs
*.ast
node_modules/

27
.gitmodules vendored Normal file
View File

@ -0,0 +1,27 @@
[submodule "validation/frameworks/php-language-server"]
path = validation/frameworks/php-language-server
url = https://github.com/felixfbecker/php-language-server
[submodule "validation/frameworks/wordpress"]
path = validation/frameworks/wordpress
url = https://github.com/wordpress/wordpress
[submodule "validation/frameworks/drupal"]
path = validation/frameworks/drupal
url = https://github.com/drupal/drupal
[submodule "validation/frameworks/tolerant-php-parser"]
path = validation/frameworks/tolerant-php-parser
url = https://github.com/microsoft/tolerant-php-parser
[submodule "validation/frameworks/symfony"]
path = validation/frameworks/symfony
url = https://github.com/symfony/symfony
[submodule "validation/frameworks/math-php"]
path = validation/frameworks/math-php
url = https://github.com/markrogoyski/math-php
[submodule "validation/frameworks/codeigniter"]
path = validation/frameworks/codeigniter
url = https://github.com/bcit-ci/codeigniter
[submodule "validation/frameworks/cakephp"]
path = validation/frameworks/cakephp
url = https://github.com/cakephp/cakephp
[submodule "validation/frameworks/phpunit"]
path = validation/frameworks/phpunit
url = https://github.com/sebastianbergmann/phpunit

View File

@ -1,28 +1,57 @@
language: php language: php
php: php:
- '7.0' - '7.0'
- '7.2'
services: git:
- docker depth: 10
submodules: false
cache: cache:
directories: directories:
- $HOME/.composer/cache - $HOME/Library/Caches/Homebrew
- $HOME/.composer/cache
- $HOME/.npm
install: install:
- composer install - composer install --prefer-dist --no-interaction
- composer run-script parse-stubs
script: script:
- vendor/bin/phpcs -n - vendor/bin/phpcs -n
- vendor/bin/phpunit --coverage-clover=coverage.xml - vendor/bin/phpunit --coverage-clover=coverage.xml --colors=always
- bash <(curl -s https://codecov.io/bash)
after_success: jobs:
- bash <(curl -s https://codecov.io/bash) include:
- | - stage: test
if [[ $TRAVIS_TAG == v* ]]; then os: osx
docker build -t felixfbecker/php-language-server:${TRAVIS_TAG:1} . osx_image: xcode9.1
docker login -e="$DOCKER_EMAIL" -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" language: generic
docker push felixfbecker/php-language-server:${TRAVIS_TAG:1} before_install:
fi # Fix ruby error https://github.com/Homebrew/brew/issues/3299
- brew update
- brew install php@7.1
- brew link --force --overwrite php@7.1
- pecl install xdebug-2.6.0
- php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
- php composer-setup.php
- ln -s "`pwd`/composer.phar" /usr/local/bin/composer
- stage: release
php: '7.0'
services:
- docker
install:
- nvm install 8
- nvm use 8
- npm install
script:
- ./node_modules/.bin/semantic-release
stages:
- test
- name: release
if: branch = master AND type = push AND fork = false
branches:
only:
- master

27
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,27 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "PHPUnit",
"type": "php",
"request": "launch",
"program": "${workspaceRoot}/vendor/phpunit/phpunit/phpunit",
// "args": ["--filter", "testDefinitionForSelfKeyword"],
"cwd": "${workspaceRoot}"
},
{
"name": "Listen for XDebug",
"type": "php",
"request": "launch",
"port": 9000
},
{
"name": "Launch currently open script",
"type": "php",
"request": "launch",
"program": "${file}",
"cwd": "${fileDirname}",
"port": 9000
}
]
}

7
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,7 @@
// Place your settings in this file to overwrite default and user settings.
{
"search.exclude": {
"**/validation": true,
"**/tests/Validation/cases": true
}
}

View File

@ -7,11 +7,6 @@
FROM php:7-cli FROM php:7-cli
MAINTAINER Felix Becker <felix.b@outlook.com> MAINTAINER Felix Becker <felix.b@outlook.com>
RUN apt-get update \
# Needed for CodeSniffer
&& apt-get install -y libxml2 libxml2-dev \
&& rm -rf /var/lib/apt/lists/*
RUN docker-php-ext-configure pcntl --enable-pcntl RUN docker-php-ext-configure pcntl --enable-pcntl
RUN docker-php-ext-install pcntl RUN docker-php-ext-install pcntl
COPY ./php.ini /usr/local/etc/php/conf.d/ COPY ./php.ini /usr/local/etc/php/conf.d/

61
Performance.php Normal file
View File

@ -0,0 +1,61 @@
<?php
namespace LanguageServer\Tests;
require __DIR__ . '/vendor/autoload.php';
use Exception;
use LanguageServer\Index\Index;
use LanguageServer\PhpDocument;
use LanguageServer\DefinitionResolver;
use Microsoft\PhpParser;
use phpDocumentor\Reflection\DocBlockFactory;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
$totalSize = 0;
$frameworks = ["drupal", "wordpress", "php-language-server", "tolerant-php-parser", "math-php", "symfony", "codeigniter", "cakephp"];
foreach($frameworks as $framework) {
$iterator = new RecursiveDirectoryIterator(__DIR__ . "/validation/frameworks/$framework");
$testProviderArray = array();
foreach (new RecursiveIteratorIterator($iterator) as $file) {
if (strpos((string)$file, ".php") !== false) {
$totalSize += $file->getSize();
$testProviderArray[] = $file->getPathname();
}
}
if (count($testProviderArray) === 0) {
throw new Exception("ERROR: Validation testsuite frameworks not found - run `git submodule update --init --recursive` to download.");
}
$start = microtime(true);
foreach ($testProviderArray as $idx => $testCaseFile) {
if (filesize($testCaseFile) > 10000) {
continue;
}
if ($idx % 1000 === 0) {
echo "$idx\n";
}
$fileContents = file_get_contents($testCaseFile);
$docBlockFactory = DocBlockFactory::createInstance();
$index = new Index;
$maxRecursion = [];
$definitions = [];
$definitionResolver = new DefinitionResolver($index);
$parser = new PhpParser\Parser();
$document = new PhpDocument($testCaseFile, $fileContents, $index, $parser, $docBlockFactory, $definitionResolver);
}
echo "------------------------------\n";
echo "Time [$framework]: " . (microtime(true) - $start) . PHP_EOL;
}

View File

@ -1,9 +1,11 @@
# PHP Language Server # PHP Language Server
[![Version](https://img.shields.io/packagist/v/felixfbecker/language-server.svg)](https://packagist.org/packages/felixfbecker/language-server) [![Version](https://img.shields.io/packagist/v/felixfbecker/language-server.svg)](https://packagist.org/packages/felixfbecker/language-server)
[![Build Status](https://travis-ci.org/felixfbecker/php-language-server.svg?branch=master)](https://travis-ci.org/felixfbecker/php-language-server) [![Linux Build Status](https://travis-ci.org/felixfbecker/php-language-server.svg?branch=master)](https://travis-ci.org/felixfbecker/php-language-server)
[![Windows Build status](https://ci.appveyor.com/api/projects/status/2sp5ll052wdjqmdm/branch/master?svg=true)](https://ci.appveyor.com/project/felixfbecker/php-language-server/branch/master)
[![Coverage](https://codecov.io/gh/felixfbecker/php-language-server/branch/master/graph/badge.svg)](https://codecov.io/gh/felixfbecker/php-language-server) [![Coverage](https://codecov.io/gh/felixfbecker/php-language-server/branch/master/graph/badge.svg)](https://codecov.io/gh/felixfbecker/php-language-server)
[![Dependency Status](https://gemnasium.com/badges/github.com/felixfbecker/php-language-server.svg)](https://gemnasium.com/github.com/felixfbecker/php-language-server) [![Dependency Status](https://gemnasium.com/badges/github.com/felixfbecker/php-language-server.svg)](https://gemnasium.com/github.com/felixfbecker/php-language-server)
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
[![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%207.0-8892BF.svg)](https://php.net/) [![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%207.0-8892BF.svg)](https://php.net/)
[![License](https://img.shields.io/packagist/l/felixfbecker/language-server.svg)](https://github.com/felixfbecker/php-language-server/blob/master/LICENSE.txt) [![License](https://img.shields.io/packagist/l/felixfbecker/language-server.svg)](https://github.com/felixfbecker/php-language-server/blob/master/LICENSE.txt)
[![Gitter](https://badges.gitter.im/felixfbecker/php-language-server.svg)](https://gitter.im/felixfbecker/php-language-server?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Gitter](https://badges.gitter.im/felixfbecker/php-language-server.svg)](https://gitter.im/felixfbecker/php-language-server?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
@ -11,12 +13,28 @@
A pure PHP implementation of the open [Language Server Protocol](https://github.com/Microsoft/language-server-protocol). A pure PHP implementation of the open [Language Server Protocol](https://github.com/Microsoft/language-server-protocol).
Provides static code analysis for PHP for any IDE. Provides static code analysis for PHP for any IDE.
Uses the great [PHP-Parser](https://github.com/nikic/PHP-Parser), Uses the great [Tolerant PHP Parser](https://github.com/Microsoft/tolerant-php-parser),
[phpDocumentor's DocBlock reflection](https://github.com/phpDocumentor/ReflectionDocBlock) [phpDocumentor's DocBlock reflection](https://github.com/phpDocumentor/ReflectionDocBlock)
and an [event loop](http://sabre.io/event/loop/) for concurrency. and an [event loop](http://sabre.io/event/loop/) for concurrency.
**Table of Contents**
- [Features](#features)
- [Performance](#performance)
- [Versioning](#versioning)
- [Installation](#installation)
- [Running](#running)
- [Used by](#used-by)
- [Contributing](#contributing)
## Features ## Features
### [Completion](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#textDocument_completion)
![Completion search demo](images/completion.gif)
### [Signature Help](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#textDocument_signatureHelp)
![Signature help demo](images/signatureHelp.gif)
### [Go To Definition](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#goto-definition-request) ### [Go To Definition](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#goto-definition-request)
![Go To Definition demo](images/definition.gif) ![Go To Definition demo](images/definition.gif)
@ -40,9 +58,6 @@ For Parameters, it will return the `@param` tag.
The query is matched case-insensitively against the fully qualified name of the symbol. The query is matched case-insensitively against the fully qualified name of the symbol.
Non-Standard: An empty query will return _all_ symbols found in the workspace. Non-Standard: An empty query will return _all_ symbols found in the workspace.
### [Document Formatting](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#document-formatting-request)
![Document Formatting demo](images/formatDocument.gif)
### Error reporting through [Publish Diagnostics](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#publishdiagnostics-notification) ### Error reporting through [Publish Diagnostics](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#publishdiagnostics-notification)
![Error reporting demo](images/publishDiagnostics.png) ![Error reporting demo](images/publishDiagnostics.png)
@ -170,7 +185,7 @@ Example:
#### `--memory-limit=integer` (optional) #### `--memory-limit=integer` (optional)
Sets memory limit for language server. Sets memory limit for language server.
Equivalent to [memory-limit](http://php.net/manual/en/ini.core.php#ini.memory-limit) php.ini directive. Equivalent to [memory-limit](http://php.net/manual/en/ini.core.php#ini.memory-limit) php.ini directive.
By default there is no memory limit. The default is 4GB (which is way more than needed).
Example: Example:
@ -180,7 +195,8 @@ Example:
- [VS Code PHP IntelliSense](https://github.com/felixfbecker/vscode-php-intellisense) - [VS Code PHP IntelliSense](https://github.com/felixfbecker/vscode-php-intellisense)
- [Eclipse Che](https://eclipse.org/che/) - [Eclipse Che](https://eclipse.org/che/)
- [Eclipse IDE (LSP4E-PHP)](https://github.com/eclipselabs/lsp4e-php) - [Eclipse IDE (LSP4E-PHP)](https://github.com/eclipselabs/lsp4e-php)
- [Neovim (nvim-cm-php-language-server)](https://github.com/roxma/nvim-cm-php-language-server) - NeoVim: [LanguageServer-php-neovim](https://github.com/roxma/LanguageServer-php-neovim) with [LanguageClient neovim](https://github.com/autozimu/LanguageClient-neovim)
- Atom: [ide-php](https://github.com/atom/ide-php)
## Contributing ## Contributing
@ -190,14 +206,21 @@ Clone the repository and run
composer install composer install
to install dependencies. to install dependencies.
Then parse the stubs with
composer run-script parse-stubs
Run the tests with Run the tests with
vendor/bin/phpunit composer test
Lint with Lint with
vendor/bin/phpcs composer lint
The project parses PHPStorm's PHP stubs to get support for PHP builtins. It re-parses them as needed after Composer processes, but after some code changes (such as ones involving the index or parsing) you may have to explicitly re-parse them:
composer run-script parse-stubs
To debug with xDebug ensure that you have this set as an environment variable
PHPLS_ALLOW_XDEBUG=1
This tells the Language Server to not restart without XDebug if it detects that XDebug is enabled (XDebug has a high performance impact).

54
appveyor.yml Normal file
View File

@ -0,0 +1,54 @@
version: '{build}'
image: Visual Studio 2017
platform:
- x64
skip_tags: true
skip_branch_with_pr: true
clone_depth: 1
max_jobs: 3
cache:
- '%LOCALAPPDATA%\Composer'
- '%LOCALAPPDATA%\Temp\Chocolatey'
environment:
ANSICON: 121x90 (121x90)
matrix:
- { PHP_VERSION: '7.1.11', VC_VERSION: '14', XDEBUG_VERSION: '2.5.5' }
install:
# Enable Windows Update service, needed to install vcredist2015 (dependency of php)
- ps: Set-Service wuauserv -StartupType Manual
- choco config set cacheLocation %LOCALAPPDATA%\Temp\Chocolatey
- choco install -y php --version %PHP_VERSION%
- choco install -y composer
- refreshenv
- composer install --no-interaction --no-progress --prefer-dist
# Install XDebug for code coverage
- ps: |
$client = New-Object System.Net.WebClient
$phpMinorVersion = $env:PHP_VERSION -replace '\.\d+$'
$xdebugUrl = "https://xdebug.org/files/php_xdebug-$env:XDEBUG_VERSION-$phpMinorVersion-vc14-nts-x86_64.dll"
$phpDir = (Get-Item (Get-Command php).Source).Directory.FullName
$xdebugPath = Join-Path $phpDir ext\xdebug.dll
$client.DownloadFile($xdebugUrl, $xdebugPath)
$phpIniPath = Join-Path $phpDir php.ini
Add-Content $phpIniPath @"
zend_extension=$xdebugPath
"@
build: off
test_script:
- vendor\bin\phpunit --coverage-clover=coverage/coverage.xml
after_test:
- ps: |
# Delete vendor because it causes problems with codecovs report search
# https://github.com/codecov/codecov-bash/issues/96
Remove-Item -Recurse -Force vendor
$env:PATH = 'C:\msys64\usr\bin;' + $env:PATH
Invoke-WebRequest -Uri 'https://codecov.io/bash' -OutFile codecov.sh
bash codecov.sh -f 'coverage/coverage.xml'

View File

@ -1,12 +1,12 @@
<?php <?php
use LanguageServer\{LanguageServer, ProtocolStreamReader, ProtocolStreamWriter}; use LanguageServer\{LanguageServer, ProtocolStreamReader, ProtocolStreamWriter, StderrLogger};
use Sabre\Event\Loop; use Sabre\Event\Loop;
use Composer\{Factory, XdebugHandler}; use Composer\XdebugHandler\XdebugHandler;
$options = getopt('', ['tcp::', 'tcp-server::', 'memory-limit::']); $options = getopt('', ['tcp::', 'tcp-server::', 'memory-limit::']);
ini_set('memory_limit', $options['memory-limit'] ?? -1); ini_set('memory_limit', $options['memory-limit'] ?? '4G');
foreach ([__DIR__ . '/../../../autoload.php', __DIR__ . '/../autoload.php', __DIR__ . '/../vendor/autoload.php'] as $file) { foreach ([__DIR__ . '/../../../autoload.php', __DIR__ . '/../autoload.php', __DIR__ . '/../vendor/autoload.php'] as $file) {
if (file_exists($file)) { if (file_exists($file)) {
@ -24,22 +24,27 @@ set_error_handler(function (int $severity, string $message, string $file, int $l
throw new \ErrorException($message, 0, $severity, $file, $line); throw new \ErrorException($message, 0, $severity, $file, $line);
}); });
$logger = new StderrLogger();
// Only write uncaught exceptions to STDERR, not STDOUT // Only write uncaught exceptions to STDERR, not STDOUT
set_exception_handler(function (\Throwable $e) { set_exception_handler(function (\Throwable $e) use ($logger) {
fwrite(STDERR, (string)$e); $logger->critical((string)$e);
}); });
@cli_set_process_title('PHP Language Server'); @cli_set_process_title('PHP Language Server');
// If XDebug is enabled, restart without it // If XDebug is enabled, restart without it
(new XdebugHandler(Factory::createOutput()))->check(); $xdebugHandler = new XdebugHandler('PHPLS');
$xdebugHandler->setLogger($logger);
$xdebugHandler->check();
unset($xdebugHandler);
if (!empty($options['tcp'])) { if (!empty($options['tcp'])) {
// Connect to a TCP server // Connect to a TCP server
$address = $options['tcp']; $address = $options['tcp'];
$socket = stream_socket_client('tcp://' . $address, $errno, $errstr); $socket = stream_socket_client('tcp://' . $address, $errno, $errstr);
if ($socket === false) { if ($socket === false) {
fwrite(STDERR, "Could not connect to language client. Error $errno\n$errstr"); $logger->critical("Could not connect to language client. Error $errno\n$errstr");
exit(1); exit(1);
} }
stream_set_blocking($socket, false); stream_set_blocking($socket, false);
@ -53,29 +58,30 @@ if (!empty($options['tcp'])) {
$address = $options['tcp-server']; $address = $options['tcp-server'];
$tcpServer = stream_socket_server('tcp://' . $address, $errno, $errstr); $tcpServer = stream_socket_server('tcp://' . $address, $errno, $errstr);
if ($tcpServer === false) { if ($tcpServer === false) {
fwrite(STDERR, "Could not listen on $address. Error $errno\n$errstr"); $logger->critical("Could not listen on $address. Error $errno\n$errstr");
exit(1); exit(1);
} }
fwrite(STDOUT, "Server listening on $address\n"); $logger->debug("Server listening on $address");
if (!extension_loaded('pcntl')) { $pcntlAvailable = extension_loaded('pcntl');
fwrite(STDERR, "PCNTL is not available. Only a single connection will be accepted\n"); if (!$pcntlAvailable) {
$logger->notice('PCNTL is not available. Only a single connection will be accepted');
} }
while ($socket = stream_socket_accept($tcpServer, -1)) { while ($socket = stream_socket_accept($tcpServer, -1)) {
fwrite(STDOUT, "Connection accepted\n"); $logger->debug('Connection accepted');
stream_set_blocking($socket, false); stream_set_blocking($socket, false);
if (extension_loaded('pcntl')) { if ($pcntlAvailable) {
// If PCNTL is available, fork a child process for the connection // If PCNTL is available, fork a child process for the connection
// An exit notification will only terminate the child process // An exit notification will only terminate the child process
$pid = pcntl_fork(); $pid = pcntl_fork();
if ($pid === -1) { if ($pid === -1) {
fwrite(STDERR, "Could not fork\n"); $logger->critical('Could not fork');
exit(1); exit(1);
} else if ($pid === 0) { } else if ($pid === 0) {
// Child process // Child process
$reader = new ProtocolStreamReader($socket); $reader = new ProtocolStreamReader($socket);
$writer = new ProtocolStreamWriter($socket); $writer = new ProtocolStreamWriter($socket);
$reader->on('close', function () { $reader->on('close', function () use ($logger) {
fwrite(STDOUT, "Connection closed\n"); $logger->debug('Connection closed');
}); });
$ls = new LanguageServer($reader, $writer); $ls = new LanguageServer($reader, $writer);
Loop\run(); Loop\run();
@ -94,6 +100,7 @@ if (!empty($options['tcp'])) {
} }
} else { } else {
// Use STDIO // Use STDIO
$logger->debug('Listening on STDIN');
stream_set_blocking(STDIN, false); stream_set_blocking(STDIN, false);
$ls = new LanguageServer( $ls = new LanguageServer(
new ProtocolStreamReader(STDIN), new ProtocolStreamReader(STDIN),

View File

@ -1,14 +1,7 @@
{ {
"name": "felixfbecker/language-server", "name": "felixfbecker/language-server",
"description": "PHP Implementation of the Visual Studio Code Language Server Protocol", "description": "PHP Implementation of the Visual Studio Code Language Server Protocol",
"authors": [
{
"name": "Felix Becker",
"email": "felix.b@outlook.com"
}
],
"license": "ISC", "license": "ISC",
"type": "library",
"keywords": [ "keywords": [
"php", "php",
"language", "language",
@ -21,33 +14,38 @@
"autocompletion", "autocompletion",
"refactor" "refactor"
], ],
"bin": ["bin/php-language-server.php"], "authors": [
"scripts": { {
"parse-stubs": "LanguageServer\\ComposerScripts::parseStubs", "name": "Felix Becker",
"post-install-cmd": "@parse-stubs" "email": "felix.b@outlook.com"
}, }
],
"require": { "require": {
"php": ">=7.0", "php": "^7.0",
"nikic/php-parser": "^3.0.4", "composer/xdebug-handler": "^1.0",
"phpdocumentor/reflection-docblock": "^3.0", "felixfbecker/advanced-json-rpc": "^3.0.0",
"sabre/event": "^5.0",
"felixfbecker/advanced-json-rpc": "^2.0",
"squizlabs/php_codesniffer" : "3.0.0RC3",
"netresearch/jsonmapper": "^1.0",
"webmozart/path-util": "^2.3",
"webmozart/glob": "^4.1",
"sabre/uri": "^2.0",
"jetbrains/phpstorm-stubs": "dev-master", "jetbrains/phpstorm-stubs": "dev-master",
"composer/composer": "^1.3" "microsoft/tolerant-php-parser": "0.0.*",
"netresearch/jsonmapper": "^1.0",
"phpdocumentor/reflection-docblock": "^4.0.0",
"psr/log": "^1.0",
"sabre/event": "^5.0",
"sabre/uri": "^2.0",
"webmozart/glob": "^4.1",
"webmozart/path-util": "^2.3"
},
"require-dev": {
"phpunit/phpunit": "^6.3",
"squizlabs/php_codesniffer": "^3.1"
}, },
"minimum-stability": "dev",
"prefer-stable": true,
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"LanguageServer\\": "src/" "LanguageServer\\": "src/"
}, },
"files" : [ "files" : [
"src/utils.php" "src/utils.php",
"src/FqnUtilities.php",
"src/ParserHelpers.php"
] ]
}, },
"autoload-dev": { "autoload-dev": {
@ -55,8 +53,19 @@
"LanguageServer\\Tests\\": "tests/" "LanguageServer\\Tests\\": "tests/"
} }
}, },
"require-dev": { "bin": [
"phpunit/phpunit": "^5.5", "bin/php-language-server.php"
"phpunit/php-code-coverage": "^4.0" ],
} "scripts": {
"parse-stubs": "LanguageServer\\ComposerScripts::parseStubs",
"post-install-cmd": "@parse-stubs",
"post-update-cmd": "@parse-stubs",
"test": "vendor/bin/phpunit",
"lint": "vendor/bin/phpcs"
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true
} }

17
dependencies.yml Normal file
View File

@ -0,0 +1,17 @@
collectors:
# pull requests for new major versions
- type: php-composer
path: /
actors:
- type: php-composer
versions: "Y.0.0"
settings:
commit_message_prefix: "chore: "
- type: js-npm
path: /
actors:
- type: js-npm
versions: "Y.0.0"
settings:
commit_message_prefix: "chore: "

View File

@ -0,0 +1,23 @@
<?php
namespace HELLO {
/**
* Does something really cool!
*/
function world() {
}
\HE
}
namespace {
/**
* Lorem ipsum dolor sit amet.
*/
define('HELLO', true);
HELLO\world();
}

View File

@ -0,0 +1,46 @@
<?php
namespace Foo;
class Bar {
public $foo;
/** @return Bar[] */
public function test() { }
}
$bar = new Bar();
$bars = $bar->test();
$array1 = [new Bar(), new \stdClass()];
$array2 = ['foo' => $bar, $bar];
$array3 = ['foo' => $bar, 'baz' => $bar];
foreach ($bars as $value) {
$v
$value->
}
foreach ($array1 as $key => $value) {
$
}
foreach ($array2 as $key => $value) {
$
}
foreach ($array3 as $key => $value) {
$
}
foreach ($bar->test() as $value) {
$
}
foreach ($unknownArray as $member->access => $unknown) {
$unkno
foreach ($loop as $loop) {
}
foreach ($loop->getArray() as $loop) {
}

View File

@ -0,0 +1 @@
<

View File

@ -0,0 +1,11 @@
<?php
namespace MyNamespace;
class SomeClass
{
public function someMethod()
{
tes
}
}

View File

@ -0,0 +1,11 @@
<?php
class FooClass {
public function foo(): FooClass {
return $this;
}
}
$fc = new FooClass();
$foo = $fc->foo();
$foo->

View File

@ -0,0 +1,12 @@
<?php
class FooClass {
public static function staticFoo(): FooClass {
return new FooClass();
}
public function bar() { }
}
$foo = FooClass::staticFoo();
$foo->

View File

@ -0,0 +1,15 @@
<?php
class ThisClass
{
private $foo;
private $bar;
protected function method()
{
}
public function test()
{
$this->
}
}

View File

@ -0,0 +1,23 @@
<?php
class Grand
{
/** @return $this */
public function foo()
{
return $this;
}
}
class Parent1 extends Grand
{
}
class Child extends Parent1
{
public function bar()
{
$this->foo()->q
}
public function qux()
{
}
}

View File

@ -0,0 +1,15 @@
<?php
class ThisClassPrefix extends TestClass
{
private $foo;
private $bar;
protected function method()
{
}
public function test()
{
$this->m
}
}

View File

@ -0,0 +1,9 @@
<?php
class Foo
{
public function bar()
{
return $this;
}
}

View File

@ -0,0 +1,9 @@
<?php
class Foo
{
public static function bar()
{
return $this;
}
}

View File

@ -1,20 +0,0 @@
<?php
namespace TestNamespace;
use SomeNamespace\Goo;
class TestClass
{
public $testProperty;
public function testMethod($testParameter)
{
$testVariable = 123;
if (empty($testParameter)){
echo 'Empty';
}
}
}

View File

@ -98,3 +98,22 @@ new class {
}; };
class ChildClass extends TestClass {} class ChildClass extends TestClass {}
/**
* Lorem ipsum dolor sit amet, consectetur.
*/
define('TEST_DEFINE_CONSTANT', false);
print TEST_DEFINE_CONSTANT ? 'true' : 'false';
/**
* Neither this class nor its members are referenced anywhere
*/
class UnusedClass
{
public $unusedProperty;
public function unusedMethod()
{
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace Foo;
class Test
{
/**
* Constructor comment goes here
*
* @param string $first First param
* @param int $second Second param
* @param Test $third Third param with a longer description
*/
public function __construct(string $first, int $second, Test $third)
{
}
/**
* Function doc
*
* @param SomethingElse $a A param with a different doc type
* @param int|null $b Param with default value
*/
public function foo(\DateTime $a, int $b = null)
{
}
public static function bar($a)
{
}
/**
* Method with no params
*/
public function baz()
{
}
}
/**
* @param int $i Global function param one
* @param bool $b Default false param
* @param Test|null ...$things Test things
*/
function foo(int $i, bool $b = false, Test ...$things = null)
{
}
$t = new Test();
$t = new Test(1, );
$t->foo();
$t->foo(1,
$t->foo(1,);
$t->baz();
foo(
1,
foo(1, 2,
);
Test::bar();
new $foo();
new $foo(1, );
new NotExist();

View File

@ -98,3 +98,8 @@ new class {
}; };
class ChildClass extends TestClass {} class ChildClass extends TestClass {}
class Example {
public function __construct() {}
public function __destruct() {}
}

BIN
images/completion.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

BIN
images/signatureHelp.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

6662
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

31
package.json Normal file
View File

@ -0,0 +1,31 @@
{
"private": true,
"repository": {
"type": "git",
"url": "https://github.com/felixfbecker/php-language-server.git"
},
"devDependencies": {
"@semantic-release/exec": "^3.1.0",
"semantic-release": "^15.9.9",
"semantic-release-docker": "^2.1.0"
},
"release": {
"verifyConditions": [
"@semantic-release/github",
"semantic-release-docker"
],
"prepare": [
{
"path": "@semantic-release/exec",
"cmd": "composer install --prefer-dist --no-interaction && docker build -t felixfbecker/php-language-server ."
}
],
"publish": [
"@semantic-release/github",
{
"path": "semantic-release-docker",
"name": "felixfbecker/php-language-server"
}
]
}
}

View File

@ -2,9 +2,12 @@
<ruleset name="PHP Language Server"> <ruleset name="PHP Language Server">
<file>src</file> <file>src</file>
<file>tests</file> <file>tests</file>
<exclude-pattern>tests/Validation/cases</exclude-pattern>
<rule ref="PSR2"> <rule ref="PSR2">
<exclude name="PSR2.Namespaces.UseDeclaration.MultipleDeclarations"/> <exclude name="PSR2.Namespaces.UseDeclaration.MultipleDeclarations"/>
<exclude name="PSR2.ControlStructures.ElseIfDeclaration.NotAllowed"/> <exclude name="PSR2.ControlStructures.ElseIfDeclaration.NotAllowed"/>
<exclude name="PSR2.ControlStructures.ControlStructureSpacing.SpacingAfterOpenBrace"/> <exclude name="PSR2.ControlStructures.ControlStructureSpacing.SpacingAfterOpenBrace"/>
<exclude name="Squiz.WhiteSpace.ControlStructureSpacing.SpacingBeforeClose"/>
<exclude name="Squiz.WhiteSpace.ControlStructureSpacing.SpacingAfterOpen"/>
</rule> </rule>
</ruleset> </ruleset>

View File

@ -1,14 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"> <phpunit backupGlobals="false"
backupStaticAttributes="false"
beStrictAboutTestsThatDoNotTestAnything="true"
beStrictAboutOutputDuringTests="true"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
failOnWarning="true"
processIsolation="false"
stopOnError="false"
stopOnFailure="false"
verbose="true"
>
<testsuites> <testsuites>
<testsuite name="PHP Language Server Test Suite"> <testsuite name="PHP Language Server Test Suite">
<directory>./tests</directory> <directory suffix="Test.php">./tests</directory>
</testsuite> </testsuite>
</testsuites> </testsuites>
<filter> <filter>
<whitelist> <whitelist processUncoveredFilesFromWhitelist="true">
<directory>./src</directory> <directory suffix=".php">./src</directory>
</whitelist> </whitelist>
</filter> </filter>
<php>
<ini name="memory_limit" value="1024M"/>
</php>
</phpunit> </phpunit>

View File

@ -3,7 +3,6 @@ declare(strict_types = 1);
namespace LanguageServer\Cache; namespace LanguageServer\Cache;
use LanguageServer\LanguageClient;
use Sabre\Event\Promise; use Sabre\Event\Promise;
/** /**

View File

@ -4,7 +4,7 @@ declare(strict_types = 1);
namespace LanguageServer\Client; namespace LanguageServer\Client;
use LanguageServer\ClientHandler; use LanguageServer\ClientHandler;
use LanguageServer\Protocol\{Message, TextDocumentItem, TextDocumentIdentifier}; use LanguageServer\Protocol\{TextDocumentItem, TextDocumentIdentifier};
use Sabre\Event\Promise; use Sabre\Event\Promise;
use JsonMapper; use JsonMapper;

View File

@ -4,7 +4,6 @@ declare(strict_types = 1);
namespace LanguageServer\Client; namespace LanguageServer\Client;
use LanguageServer\ClientHandler; use LanguageServer\ClientHandler;
use LanguageServer\Protocol\Message;
use Sabre\Event\Promise; use Sabre\Event\Promise;
/** /**

View File

@ -4,7 +4,6 @@ declare(strict_types = 1);
namespace LanguageServer\Client; namespace LanguageServer\Client;
use LanguageServer\ClientHandler; use LanguageServer\ClientHandler;
use LanguageServer\Protocol\Message;
use Sabre\Event\Promise; use Sabre\Event\Promise;
/** /**

View File

@ -71,7 +71,6 @@ class ClientHandler
*/ */
public function notify(string $method, $params): Promise public function notify(string $method, $params): Promise
{ {
$id = $this->idGenerator->generate();
return $this->protocolWriter->write( return $this->protocolWriter->write(
new Protocol\Message( new Protocol\Message(
new AdvancedJsonRpc\Notification($method, (object)$params) new AdvancedJsonRpc\Notification($method, (object)$params)

View File

@ -3,7 +3,6 @@ declare(strict_types = 1);
namespace LanguageServer; namespace LanguageServer;
use PhpParser\Node;
use LanguageServer\Index\ReadableIndex; use LanguageServer\Index\ReadableIndex;
use LanguageServer\Protocol\{ use LanguageServer\Protocol\{
TextEdit, TextEdit,
@ -11,8 +10,13 @@ use LanguageServer\Protocol\{
Position, Position,
CompletionList, CompletionList,
CompletionItem, CompletionItem,
CompletionItemKind CompletionItemKind,
CompletionContext,
CompletionTriggerKind
}; };
use Microsoft\PhpParser;
use Microsoft\PhpParser\Node;
use Generator;
class CompletionProvider class CompletionProvider
{ {
@ -48,6 +52,7 @@ class CompletionProvider
'eval', 'eval',
'exit', 'exit',
'extends', 'extends',
'false',
'final', 'final',
'finally', 'finally',
'for', 'for',
@ -66,6 +71,7 @@ class CompletionProvider
'list', 'list',
'namespace', 'namespace',
'new', 'new',
'null',
'or', 'or',
'print', 'print',
'private', 'private',
@ -78,13 +84,30 @@ class CompletionProvider
'switch', 'switch',
'throw', 'throw',
'trait', 'trait',
'true',
'try', 'try',
'unset', 'unset',
'use', 'use',
'var', 'var',
'while', 'while',
'xor', 'xor',
'yield' 'yield',
// List of other reserved words (http://php.net/manual/en/reserved.other-reserved-words.php)
// (the ones which do not occur as actual keywords above.)
'int',
'float',
'bool',
'string',
'void',
'iterable',
'object',
// Pseudo keywords
'from', // As in yield from
'strict_types',
'ticks', // As in declare(ticks=1)
'encoding', // As in declare(encoding='EBCDIC')
]; ];
/** /**
@ -104,7 +127,7 @@ class CompletionProvider
/** /**
* @param DefinitionResolver $definitionResolver * @param DefinitionResolver $definitionResolver
* @param ReadableIndex $index * @param ReadableIndex $index
*/ */
public function __construct(DefinitionResolver $definitionResolver, ReadableIndex $index) public function __construct(DefinitionResolver $definitionResolver, ReadableIndex $index)
{ {
@ -117,159 +140,82 @@ class CompletionProvider
* *
* @param PhpDocument $doc The opened document * @param PhpDocument $doc The opened document
* @param Position $pos The cursor position * @param Position $pos The cursor position
* @param CompletionContext $context The completion context
* @return CompletionList * @return CompletionList
*/ */
public function provideCompletion(PhpDocument $doc, Position $pos): CompletionList public function provideCompletion(PhpDocument $doc, Position $pos, CompletionContext $context = null): CompletionList
{ {
// This can be made much more performant if the tree follows specific invariants.
$node = $doc->getNodeAtPosition($pos); $node = $doc->getNodeAtPosition($pos);
if ($node instanceof Node\Expr\Error) { // Get the node at the position under the cursor
$node = $node->getAttribute('parentNode'); $offset = $node === null ? -1 : $pos->toOffset($node->getFileContents());
if (
$node !== null
&& $offset > $node->getEndPosition()
&& $node->parent !== null
&& $node->parent->getLastChild() instanceof PhpParser\MissingToken
) {
$node = $node->parent;
} }
$list = new CompletionList; $list = new CompletionList;
$list->isIncomplete = true; $list->isIncomplete = true;
// A non-free node means we do NOT suggest global symbols if ($node instanceof Node\Expression\Variable &&
if ( $node->parent instanceof Node\Expression\ObjectCreationExpression &&
$node instanceof Node\Expr\MethodCall $node->name instanceof PhpParser\MissingToken
|| $node instanceof Node\Expr\PropertyFetch
|| $node instanceof Node\Expr\StaticCall
|| $node instanceof Node\Expr\StaticPropertyFetch
|| $node instanceof Node\Expr\ClassConstFetch
) { ) {
// If the name is an Error node, just filter by the class $node = $node->parent;
if ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\PropertyFetch) { }
// For instances, resolve the variable type
$prefixes = DefinitionResolver::getFqnsFromType(
$this->definitionResolver->resolveExpressionNodeToType($node->var)
);
} else {
// Static member reference
$prefixes = [$node->class instanceof Node\Name ? (string)$node->class : ''];
}
$prefixes = $this->expandParentFqns($prefixes);
// If we are just filtering by the class, add the appropiate operator to the prefix
// to filter the type of symbol
foreach ($prefixes as &$prefix) {
if ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\PropertyFetch) {
$prefix .= '->';
} else if ($node instanceof Node\Expr\StaticCall || $node instanceof Node\Expr\ClassConstFetch) {
$prefix .= '::';
} else if ($node instanceof Node\Expr\StaticPropertyFetch) {
$prefix .= '::$';
}
}
unset($prefix);
foreach ($this->index->getDefinitions() as $fqn => $def) { // Inspect the type of expression under the cursor
foreach ($prefixes as $prefix) {
if (substr($fqn, 0, strlen($prefix)) === $prefix && !$def->isGlobal) { $content = $doc->getContent();
$list->items[] = CompletionItem::fromDefinition($def); $offset = $pos->toOffset($content);
} if (
} $node === null
} || (
} else if ( $node instanceof Node\Statement\InlineHtml
// A ConstFetch means any static reference, like a class, interface, etc. or keyword && (
($node instanceof Node\Name && $node->getAttribute('parentNode') instanceof Node\Expr\ConstFetch) $context !== null
|| $node instanceof Node\Expr\New_ // Make sure to not suggest on the > trigger character in HTML
) {
$prefix = '';
$prefixLen = 0;
if ($node instanceof Node\Name) {
$isFullyQualified = $node->isFullyQualified();
$prefix = (string)$node;
$prefixLen = strlen($prefix);
$namespacedPrefix = (string)$node->getAttribute('namespacedName');
$namespacedPrefixLen = strlen($prefix);
}
// Find closest namespace
$namespace = getClosestNode($node, Node\Stmt\Namespace_::class);
/** Map from alias to Definition */
$aliasedDefs = [];
if ($namespace) {
foreach ($namespace->stmts as $stmt) {
if ($stmt instanceof Node\Stmt\Use_ || $stmt instanceof Node\Stmt\GroupUse) {
foreach ($stmt->uses as $use) {
// Get the definition for the used namespace, class-like, function or constant
// And save it under the alias
$fqn = (string)Node\Name::concat($stmt->prefix ?? null, $use->name);
if ($def = $this->index->getDefinition($fqn)) {
$aliasedDefs[$use->alias] = $def;
}
}
} else {
// Use statements are always the first statements in a namespace
break;
}
}
}
// If there is a prefix that does not start with a slash, suggest `use`d symbols
if ($prefix && !$isFullyQualified) {
// Suggest symbols that have been `use`d
// Search the aliases for the typed-in name
foreach ($aliasedDefs as $alias => $def) {
if (substr($alias, 0, $prefixLen) === $prefix) {
$list->items[] = CompletionItem::fromDefinition($def);
}
}
}
// Additionally, suggest global symbols that either
// - start with the current namespace + prefix, if the Name node is not fully qualified
// - start with just the prefix, if the Name node is fully qualified
foreach ($this->index->getDefinitions() as $fqn => $def) {
if (
$def->isGlobal // exclude methods, properties etc.
&& ( && (
!$prefix $context->triggerKind === CompletionTriggerKind::INVOKED
|| ( || $context->triggerCharacter === '<'
((!$namespace || $isFullyQualified) && substr($fqn, 0, $prefixLen) === $prefix)
|| (
$namespace
&& !$isFullyQualified
&& substr($fqn, 0, $namespacedPrefixLen) === $namespacedPrefix
)
)
) )
// Only suggest classes for `new` )
&& (!($node instanceof Node\Expr\New_) || $def->canBeInstantiated) )
) { || $pos == new Position(0, 0)
$item = CompletionItem::fromDefinition($def); ) {
// Find the shortest name to reference the symbol // HTML, beginning of file
if ($namespace && ($alias = array_search($def, $aliasedDefs, true)) !== false) {
// $alias is the name under which this definition is aliased in the current namespace // Inside HTML and at the beginning of the file, propose <?php
$item->insertText = $alias; $item = new CompletionItem('<?php', CompletionItemKind::KEYWORD);
} else if ($namespace && !($prefix && $isFullyQualified)) { $item->textEdit = new TextEdit(
// Insert the global FQN with trailing backslash new Range($pos, $pos),
$item->insertText = '\\' . $fqn; stripStringOverlap($doc->getRange(new Range(new Position(0, 0), $pos)), '<?php')
} else { );
// Insert the FQN without trailing backlash $list->items[] = $item;
$item->insertText = $fqn;
} } elseif (
$list->items[] = $item; $node instanceof Node\Expression\Variable
} && !(
} $node->parent instanceof Node\Expression\ScopedPropertyAccessExpression
// Suggest keywords && $node->parent->memberName === $node
if ($node instanceof Node\Name && $node->getAttribute('parentNode') instanceof Node\Expr\ConstFetch) { )
foreach (self::KEYWORDS as $keyword) { ) {
if (substr($keyword, 0, $prefixLen) === $prefix) { // Variables
$item = new CompletionItem($keyword, CompletionItemKind::KEYWORD); //
$item->insertText = $keyword . ' '; // $|
$list->items[] = $item; // $a|
}
}
}
} else if (
$node instanceof Node\Expr\Variable
|| ($node && $node->getAttribute('parentNode') instanceof Node\Expr\Variable)
) {
// Find variables, parameters and use statements in the scope // Find variables, parameters and use statements in the scope
// If there was only a $ typed, $node will be instanceof Node\Error $namePrefix = $node->getName() ?? '';
$namePrefix = $node instanceof Node\Expr\Variable && is_string($node->name) ? $node->name : '';
foreach ($this->suggestVariablesAtNode($node, $namePrefix) as $var) { foreach ($this->suggestVariablesAtNode($node, $namePrefix) as $var) {
$item = new CompletionItem; $item = new CompletionItem;
$item->kind = CompletionItemKind::VARIABLE; $item->kind = CompletionItemKind::VARIABLE;
$item->label = '$' . ($var instanceof Node\Expr\ClosureUse ? $var->var : $var->name); $item->label = '$' . $var->getName();
$item->documentation = $this->definitionResolver->getDocumentationFromNode($var); $item->documentation = $this->definitionResolver->getDocumentationFromNode($var);
$item->detail = (string)$this->definitionResolver->getTypeFromNode($var); $item->detail = (string)$this->definitionResolver->getTypeFromNode($var);
$item->textEdit = new TextEdit( $item->textEdit = new TextEdit(
@ -278,36 +224,205 @@ class CompletionProvider
); );
$list->items[] = $item; $list->items[] = $item;
} }
} else if ($node instanceof Node\Stmt\InlineHTML || $pos == new Position(0, 0)) {
$item = new CompletionItem('<?php', CompletionItemKind::KEYWORD); } elseif ($node instanceof Node\Expression\MemberAccessExpression) {
$item->textEdit = new TextEdit( // Member access expressions
new Range($pos, $pos), //
stripStringOverlap($doc->getRange(new Range(new Position(0, 0), $pos)), '<?php') // $a->c|
// $a->|
// Multiple prefixes for all possible types
$fqns = FqnUtilities\getFqnsFromType(
$this->definitionResolver->resolveExpressionNodeToType($node->dereferencableExpression)
); );
$list->items[] = $item;
// Add the object access operator to only get members of all parents
$prefixes = [];
foreach ($this->expandParentFqns($fqns) as $prefix) {
$prefixes[] = $prefix . '->';
}
// Collect all definitions that match any of the prefixes
foreach ($this->index->getDefinitions() as $fqn => $def) {
foreach ($prefixes as $prefix) {
if (substr($fqn, 0, strlen($prefix)) === $prefix && $def->isMember) {
$list->items[] = CompletionItem::fromDefinition($def);
}
}
}
} elseif (
($scoped = $node->parent) instanceof Node\Expression\ScopedPropertyAccessExpression ||
($scoped = $node) instanceof Node\Expression\ScopedPropertyAccessExpression
) {
// Static class members and constants
//
// A\B\C::$a|
// A\B\C::|
// A\B\C::$|
// A\B\C::foo|
//
// TODO: $a::|
// Resolve all possible types to FQNs
$fqns = FqnUtilities\getFqnsFromType(
$classType = $this->definitionResolver->resolveExpressionNodeToType($scoped->scopeResolutionQualifier)
);
// Append :: operator to only get static members of all parents
$prefixes = [];
foreach ($this->expandParentFqns($fqns) as $prefix) {
$prefixes[] = $prefix . '::';
}
// Collect all definitions that match any of the prefixes
foreach ($this->index->getDefinitions() as $fqn => $def) {
foreach ($prefixes as $prefix) {
if (substr(strtolower($fqn), 0, strlen($prefix)) === strtolower($prefix) && $def->isMember) {
$list->items[] = CompletionItem::fromDefinition($def);
}
}
}
} elseif (
ParserHelpers\isConstantFetch($node)
// Creation gets set in case of an instantiation (`new` expression)
|| ($creation = $node->parent) instanceof Node\Expression\ObjectCreationExpression
|| (($creation = $node) instanceof Node\Expression\ObjectCreationExpression)
) {
// Class instantiations, function calls, constant fetches, class names
//
// new MyCl|
// my_func|
// MY_CONS|
// MyCla|
// The name Node under the cursor
$nameNode = isset($creation) ? $creation->classTypeDesignator : $node;
/** The typed name */
$prefix = $nameNode instanceof Node\QualifiedName
? (string)PhpParser\ResolvedName::buildName($nameNode->nameParts, $nameNode->getFileContents())
: $nameNode->getText($node->getFileContents());
$prefixLen = strlen($prefix);
/** Whether the prefix is qualified (contains at least one backslash) */
$isQualified = $nameNode instanceof Node\QualifiedName && $nameNode->isQualifiedName();
/** Whether the prefix is fully qualified (begins with a backslash) */
$isFullyQualified = $nameNode instanceof Node\QualifiedName && $nameNode->isFullyQualifiedName();
/** The closest NamespaceDefinition Node */
$namespaceNode = $node->getNamespaceDefinition();
/** @var string The name of the namespace */
$namespacedPrefix = null;
if ($namespaceNode) {
$namespacedPrefix = (string)PhpParser\ResolvedName::buildName($namespaceNode->name->nameParts, $node->getFileContents()) . '\\' . $prefix;
$namespacedPrefixLen = strlen($namespacedPrefix);
}
// Get the namespace use statements
// TODO: use function statements, use const statements
/** @var string[] $aliases A map from local alias to fully qualified name */
list($aliases,,) = $node->getImportTablesForCurrentScope();
foreach ($aliases as $alias => $name) {
$aliases[$alias] = (string)$name;
}
// If there is a prefix that does not start with a slash, suggest `use`d symbols
if ($prefix && !$isFullyQualified) {
foreach ($aliases as $alias => $fqn) {
// Suggest symbols that have been `use`d and match the prefix
if (substr($alias, 0, $prefixLen) === $prefix && ($def = $this->index->getDefinition($fqn))) {
$list->items[] = CompletionItem::fromDefinition($def);
}
}
}
// Suggest global symbols that either
// - start with the current namespace + prefix, if the Name node is not fully qualified
// - start with just the prefix, if the Name node is fully qualified
foreach ($this->index->getDefinitions() as $fqn => $def) {
$fqnStartsWithPrefix = substr($fqn, 0, $prefixLen) === $prefix;
if (
// Exclude methods, properties etc.
!$def->isMember
&& (
!$prefix
|| (
// Either not qualified, but a matching prefix with global fallback
($def->roamed && !$isQualified && $fqnStartsWithPrefix)
// Or not in a namespace or a fully qualified name or AND matching the prefix
|| ((!$namespaceNode || $isFullyQualified) && $fqnStartsWithPrefix)
// Or in a namespace, not fully qualified and matching the prefix + current namespace
|| (
$namespaceNode
&& !$isFullyQualified
&& substr($fqn, 0, $namespacedPrefixLen) === $namespacedPrefix
)
)
)
// Only suggest classes for `new`
&& (!isset($creation) || $def->canBeInstantiated)
) {
$item = CompletionItem::fromDefinition($def);
// Find the shortest name to reference the symbol
if ($namespaceNode && ($alias = array_search($fqn, $aliases, true)) !== false) {
// $alias is the name under which this definition is aliased in the current namespace
$item->insertText = $alias;
} else if ($namespaceNode && !($prefix && $isFullyQualified)) {
// Insert the global FQN with leading backslash
$item->insertText = '\\' . $fqn;
} else {
// Insert the FQN without leading backlash
$item->insertText = $fqn;
}
// Don't insert the parenthesis for functions
// TODO return a snippet and put the cursor inside
if (substr($item->insertText, -2) === '()') {
$item->insertText = substr($item->insertText, 0, -2);
}
$list->items[] = $item;
}
}
// If not a class instantiation, also suggest keywords
if (!isset($creation)) {
foreach (self::KEYWORDS as $keyword) {
if (substr($keyword, 0, $prefixLen) === $prefix) {
$item = new CompletionItem($keyword, CompletionItemKind::KEYWORD);
$item->insertText = $keyword;
$list->items[] = $item;
}
}
}
} }
return $list; return $list;
} }
/** /**
* Adds the FQNs of all parent classes to an array of FQNs of classes * Yields FQNs from an array along with the FQNs of all parent classes
* *
* @param string[] $fqns * @param string[] $fqns
* @return string[] * @return Generator
*/ */
private function expandParentFqns(array $fqns): array private function expandParentFqns(array $fqns) : Generator
{ {
$expanded = $fqns;
foreach ($fqns as $fqn) { foreach ($fqns as $fqn) {
yield $fqn;
$def = $this->index->getDefinition($fqn); $def = $this->index->getDefinition($fqn);
if ($def) { if ($def !== null) {
foreach ($this->expandParentFqns($def->extends) as $parent) { foreach ($def->getAncestorDefinitions($this->index) as $name => $def) {
$expanded[] = $parent; yield $name;
} }
} }
} }
return $expanded;
} }
/** /**
@ -335,30 +450,34 @@ class CompletionProvider
// Walk the AST upwards until a scope boundary is met // Walk the AST upwards until a scope boundary is met
$level = $node; $level = $node;
while ($level && !($level instanceof Node\FunctionLike)) { while ($level && !($level instanceof PhpParser\FunctionLike)) {
// Walk siblings before the node // Walk siblings before the node
$sibling = $level; $sibling = $level;
while ($sibling = $sibling->getAttribute('previousSibling')) { while ($sibling = $sibling->getPreviousSibling()) {
// Collect all variables inside the sibling node // Collect all variables inside the sibling node
foreach ($this->findVariableDefinitionsInNode($sibling, $namePrefix) as $var) { foreach ($this->findVariableDefinitionsInNode($sibling, $namePrefix) as $var) {
$vars[$var->name] = $var; $vars[$var->getName()] = $var;
} }
} }
$level = $level->getAttribute('parentNode'); $level = $level->parent;
} }
// If the traversal ended because a function was met, // If the traversal ended because a function was met,
// also add its parameters and closure uses to the result list // also add its parameters and closure uses to the result list
if ($level instanceof Node\FunctionLike) { if ($level && $level instanceof PhpParser\FunctionLike && $level->parameters !== null) {
foreach ($level->params as $param) { foreach ($level->parameters->getValues() as $param) {
if (!isset($vars[$param->name]) && substr($param->name, 0, strlen($namePrefix)) === $namePrefix) { $paramName = $param->getName();
$vars[$param->name] = $param; if (empty($namePrefix) || strpos($paramName, $namePrefix) !== false) {
$vars[$paramName] = $param;
} }
} }
if ($level instanceof Node\Expr\Closure) {
foreach ($level->uses as $use) { if ($level instanceof Node\Expression\AnonymousFunctionCreationExpression && $level->anonymousFunctionUseClause !== null &&
if (!isset($vars[$use->var]) && substr($use->var, 0, strlen($namePrefix)) === $namePrefix) { $level->anonymousFunctionUseClause->useVariableNameList !== null) {
$vars[$use->var] = $use; foreach ($level->anonymousFunctionUseClause->useVariableNameList->getValues() as $use) {
$useName = $use->getName();
if (empty($namePrefix) || strpos($useName, $namePrefix) !== false) {
$vars[$useName] = $use;
} }
} }
} }
@ -372,38 +491,44 @@ class CompletionProvider
* *
* @param Node $node * @param Node $node
* @param string $namePrefix Prefix to filter * @param string $namePrefix Prefix to filter
* @return Node\Expr\Variable[] * @return Node\Expression\Variable[]
*/ */
private function findVariableDefinitionsInNode(Node $node, string $namePrefix = ''): array private function findVariableDefinitionsInNode(Node $node, string $namePrefix = ''): array
{ {
$vars = []; $vars = [];
// If the child node is a variable assignment, save it // If the child node is a variable assignment, save it
$parent = $node->getAttribute('parentNode');
if ( $isAssignmentToVariable = function ($node) {
$node instanceof Node\Expr\Variable return $node instanceof Node\Expression\AssignmentExpression;
&& ($parent instanceof Node\Expr\Assign || $parent instanceof Node\Expr\AssignOp) };
&& is_string($node->name) // Variable variables are of no use
&& substr($node->name, 0, strlen($namePrefix)) === $namePrefix if ($this->isAssignmentToVariableWithPrefix($node, $namePrefix)) {
) { $vars[] = $node->leftOperand;
$vars[] = $node; } elseif ($node instanceof Node\ForeachKey || $node instanceof Node\ForeachValue) {
} foreach ($node->getDescendantNodes() as $descendantNode) {
// Iterate over subnodes if ($descendantNode instanceof Node\Expression\Variable
foreach ($node->getSubNodeNames() as $attr) { && ($namePrefix === '' || strpos($descendantNode->getName(), $namePrefix) !== false)
if (!isset($node->$attr)) { ) {
continue; $vars[] = $descendantNode;
}
$children = is_array($node->$attr) ? $node->$attr : [$node->$attr];
foreach ($children as $child) {
// Dont try to traverse scalars
// Dont traverse functions, the contained variables are in a different scope
if (!($child instanceof Node) || $child instanceof Node\FunctionLike) {
continue;
} }
foreach ($this->findVariableDefinitionsInNode($child, $namePrefix) as $var) { }
$vars[] = $var; } else {
// Get all descendent variables, then filter to ones that start with $namePrefix.
// Avoiding closure usage in tight loop
foreach ($node->getDescendantNodes($isAssignmentToVariable) as $descendantNode) {
if ($this->isAssignmentToVariableWithPrefix($descendantNode, $namePrefix)) {
$vars[] = $descendantNode->leftOperand;
} }
} }
} }
return $vars; return $vars;
} }
private function isAssignmentToVariableWithPrefix(Node $node, string $namePrefix): bool
{
return $node instanceof Node\Expression\AssignmentExpression
&& $node->leftOperand instanceof Node\Expression\Variable
&& ($namePrefix === '' || strpos($node->leftOperand->getName(), $namePrefix) !== false);
}
} }

View File

@ -10,6 +10,7 @@ use phpDocumentor\Reflection\DocBlockFactory;
use Webmozart\PathUtil\Path; use Webmozart\PathUtil\Path;
use Sabre\Uri; use Sabre\Uri;
use function Sabre\Event\coroutine; use function Sabre\Event\coroutine;
use Microsoft\PhpParser;
foreach ([__DIR__ . '/../../../autoload.php', __DIR__ . '/../autoload.php', __DIR__ . '/../vendor/autoload.php'] as $file) { foreach ([__DIR__ . '/../../../autoload.php', __DIR__ . '/../autoload.php', __DIR__ . '/../vendor/autoload.php'] as $file) {
if (file_exists($file)) { if (file_exists($file)) {
@ -29,7 +30,7 @@ class ComposerScripts
$finder = new FileSystemFilesFinder; $finder = new FileSystemFilesFinder;
$contentRetriever = new FileSystemContentRetriever; $contentRetriever = new FileSystemContentRetriever;
$docBlockFactory = DocBlockFactory::createInstance(); $docBlockFactory = DocBlockFactory::createInstance();
$parser = new Parser; $parser = new PhpParser\Parser();
$definitionResolver = new DefinitionResolver($index); $definitionResolver = new DefinitionResolver($index);
$stubsLocation = null; $stubsLocation = null;
@ -55,7 +56,8 @@ class ComposerScripts
$parts['scheme'] = 'phpstubs'; $parts['scheme'] = 'phpstubs';
$uri = Uri\build($parts); $uri = Uri\build($parts);
$document = new PhpDocument($uri, $content, $index, $parser, $docBlockFactory, $definitionResolver); // Create a new document and add it to $index
new PhpDocument($uri, $content, $index, $parser, $docBlockFactory, $definitionResolver);
} }
$index->setComplete(); $index->setComplete();

View File

@ -3,10 +3,10 @@ declare(strict_types = 1);
namespace LanguageServer; namespace LanguageServer;
use PhpParser\Node; use LanguageServer\Index\ReadableIndex;
use phpDocumentor\Reflection\{Types, Type, Fqsen, TypeResolver}; use phpDocumentor\Reflection\{Types, Type, Fqsen, TypeResolver};
use LanguageServer\Protocol\SymbolInformation; use LanguageServer\Protocol\SymbolInformation;
use Exception; use Generator;
/** /**
* Class used to represent symbols * Class used to represent symbols
@ -38,12 +38,20 @@ class Definition
public $extends; public $extends;
/** /**
* Only true for classes, interfaces, traits, functions and non-class constants * False for classes, interfaces, traits, functions and non-class constants
* True for methods, properties and class constants
* This is so methods and properties are not suggested in the global scope * This is so methods and properties are not suggested in the global scope
* *
* @var bool * @var bool
*/ */
public $isGlobal; public $isMember;
/**
* True if this definition is affected by global namespace fallback (global function or global constant)
*
* @var bool
*/
public $roamed;
/** /**
* False for instance methods and properties * False for instance methods and properties
@ -60,7 +68,7 @@ class Definition
public $canBeInstantiated; public $canBeInstantiated;
/** /**
* @var Protocol\SymbolInformation * @var SymbolInformation
*/ */
public $symbolInformation; public $symbolInformation;
@ -70,7 +78,7 @@ class Definition
* For functions and methods, this is the return type. * For functions and methods, this is the return type.
* For any other declaration it will be null. * For any other declaration it will be null.
* Can also be a compound type. * Can also be a compound type.
* If it is unknown, will be Types\Mixed. * If it is unknown, will be Types\Mixed_.
* *
* @var \phpDocumentor\Type|null * @var \phpDocumentor\Type|null
*/ */
@ -89,4 +97,40 @@ class Definition
* @var string * @var string
*/ */
public $documentation; public $documentation;
/**
* Signature information if this definition is for a FunctionLike, for use in textDocument/signatureHelp
*
* @var SignatureInformation
*/
public $signatureInformation;
/**
* Yields the definitons of all ancestor classes (the Definition fqn is yielded as key)
*
* @param ReadableIndex $index the index to search for needed definitions
* @param bool $includeSelf should the first yielded value be the current definition itself
* @return Generator
*/
public function getAncestorDefinitions(ReadableIndex $index, bool $includeSelf = false): Generator
{
if ($includeSelf) {
yield $this->fqn => $this;
}
if ($this->extends !== null) {
// iterating once, storing the references and iterating again
// guarantees that closest definitions are yielded first
$definitions = [];
foreach ($this->extends as $fqn) {
$def = $index->getDefinition($fqn);
if ($def !== null) {
yield $def->fqn => $def;
$definitions[] = $def;
}
}
foreach ($definitions as $def) {
yield from $def->getAncestorDefinitions($index);
}
}
}
} }

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,11 @@ class FileSystemFilesFinder implements FilesFinder
return coroutine(function () use ($glob) { return coroutine(function () use ($glob) {
$uris = []; $uris = [];
foreach (new GlobIterator($glob) as $path) { foreach (new GlobIterator($glob) as $path) {
$uris[] = pathToUri($path); // Exclude any directories that also match the glob pattern
if (!is_dir($path)) {
$uris[] = pathToUri($path);
}
yield timeout(); yield timeout();
} }
return $uris; return $uris;

View File

@ -1,107 +0,0 @@
<?php
declare(strict_types = 1);
namespace LanguageServer;
use LanguageServer\Protocol\{
TextEdit,
Range,
Position
};
use Exception;
use PHP_CodeSniffer\{
Config,
Ruleset
};
use PHP_CodeSniffer\Files\DummyFile;
use PHP_CodeSniffer\Util\Tokens;
abstract class Formatter
{
/**
* Generate array of TextEdit changes for content formatting.
*
* @param string $content source code to format
* @param string $uri URI of document
*
* @return \LanguageServer\Protocol\TextEdit[]
* @throws \Exception
*/
public static function format(string $content, string $uri)
{
if (!defined('PHP_CODESNIFFER_CBF')) {
define('PHP_CODESNIFFER_CBF', true);
}
if (!defined('PHP_CODESNIFFER_VERBOSITY')) {
define('PHP_CODESNIFFER_VERBOSITY', false);
}
$path = uriToPath($uri);
$config = new Config(['dummy'], false);
$config->standards = self::findConfiguration($path);
// Autoload class to set up a bunch of PHP_CodeSniffer-specific token type constants
spl_autoload_call(Tokens::class);
$file = new DummyFile($content, new Ruleset($config), $config);
$file->process();
$fixed = $file->fixer->fixFile();
if (!$fixed && $file->getErrorCount() > 0) {
throw new Exception('Unable to format file');
}
$new = $file->fixer->getContents();
if ($content === $new) {
return [];
}
return [new TextEdit(new Range(new Position(0, 0), self::calculateEndPosition($content)), $new)];
}
/**
* Calculate position of last character.
*
* @param string $content document as string
*
* @return \LanguageServer\Protocol\Position
*/
private static function calculateEndPosition(string $content): Position
{
$lines = explode("\n", $content);
return new Position(count($lines) - 1, strlen(end($lines)));
}
/**
* Search for PHP_CodeSniffer configuration file at given directory or its parents.
* If no configuration found then PSR2 standard is loaded by default.
*
* @param string $path path to file or directory
* @return string[]
*/
private static function findConfiguration(string $path)
{
if (is_dir($path)) {
$currentDir = $path;
} else {
$currentDir = dirname($path);
}
do {
$default = $currentDir . DIRECTORY_SEPARATOR . 'phpcs.xml';
if (is_file($default)) {
return [$default];
}
$default = $currentDir . DIRECTORY_SEPARATOR . 'phpcs.xml.dist';
if (is_file($default)) {
return [$default];
}
$lastDir = $currentDir;
$currentDir = dirname($currentDir);
} while ($currentDir !== '.' && $currentDir !== $lastDir);
$standard = Config::getConfigData('default_standard') ?? 'PSR2';
return explode(',', $standard);
}
}

30
src/FqnUtilities.php Normal file
View File

@ -0,0 +1,30 @@
<?php
namespace LanguageServer\FqnUtilities;
use phpDocumentor\Reflection\{Type, Types};
/**
* Returns all possible FQNs in a type
*
* @param Type|null $type
* @return string[]
*/
function getFqnsFromType($type): array
{
$fqns = [];
if ($type instanceof Types\Object_) {
$fqsen = $type->getFqsen();
if ($fqsen !== null) {
$fqns[] = substr((string)$fqsen, 1);
}
}
if ($type instanceof Types\Compound) {
for ($i = 0; $t = $type->get($i); $i++) {
foreach (getFqnsFromType($t) as $fqn) {
$fqns[] = $fqn;
}
}
}
return $fqns;
}

View File

@ -117,7 +117,7 @@ class Index implements ReadableIndex, \Serializable
* Registers a definition * Registers a definition
* *
* @param string $fqn The fully qualified name of the symbol * @param string $fqn The fully qualified name of the symbol
* @param string $definition The Definition object * @param Definition $definition The Definition object
* @return void * @return void
*/ */
public function setDefinition(string $fqn, Definition $definition) public function setDefinition(string $fqn, Definition $definition)
@ -150,6 +150,17 @@ class Index implements ReadableIndex, \Serializable
return $this->references[$fqn] ?? []; return $this->references[$fqn] ?? [];
} }
/**
* For test use.
* Returns all references, keyed by fqn.
*
* @return string[][]
*/
public function getReferences(): array
{
return $this->references;
}
/** /**
* Adds a document URI as a referencee of a specific symbol * Adds a document URI as a referencee of a specific symbol
* *

View File

@ -8,16 +8,15 @@ use LanguageServer\FilesFinder\FilesFinder;
use LanguageServer\Index\{DependenciesIndex, Index}; use LanguageServer\Index\{DependenciesIndex, Index};
use LanguageServer\Protocol\MessageType; use LanguageServer\Protocol\MessageType;
use Webmozart\PathUtil\Path; use Webmozart\PathUtil\Path;
use Composer\Semver\VersionParser;
use Sabre\Event\Promise; use Sabre\Event\Promise;
use function Sabre\Event\coroutine; use function Sabre\Event\coroutine;
class Indexer class Indexer
{ {
/** /**
* @var The prefix for every cache item * @var int The prefix for every cache item
*/ */
const CACHE_VERSION = 1; const CACHE_VERSION = 2;
/** /**
* @var FilesFinder * @var FilesFinder
@ -156,7 +155,7 @@ class Indexer
$packageKey = null; $packageKey = null;
$cacheKey = null; $cacheKey = null;
$index = null; $index = null;
foreach (array_merge($this->composerLock->packages, $this->composerLock->{'packages-dev'}) as $package) { foreach (array_merge($this->composerLock->packages, (array)$this->composerLock->{'packages-dev'}) as $package) {
// Check if package name matches and version is absolute // Check if package name matches and version is absolute
// Dynamic constraints are not cached, because they can change every time // Dynamic constraints are not cached, because they can change every time
$packageVersion = ltrim($package->version, 'v'); $packageVersion = ltrim($package->version, 'v');

View File

@ -8,23 +8,19 @@ use LanguageServer\Protocol\{
ClientCapabilities, ClientCapabilities,
TextDocumentSyncKind, TextDocumentSyncKind,
Message, Message,
MessageType,
InitializeResult, InitializeResult,
SymbolInformation, CompletionOptions,
TextDocumentIdentifier, SignatureHelpOptions
CompletionOptions
}; };
use LanguageServer\FilesFinder\{FilesFinder, ClientFilesFinder, FileSystemFilesFinder}; use LanguageServer\FilesFinder\{FilesFinder, ClientFilesFinder, FileSystemFilesFinder};
use LanguageServer\ContentRetriever\{ContentRetriever, ClientContentRetriever, FileSystemContentRetriever}; use LanguageServer\ContentRetriever\{ContentRetriever, ClientContentRetriever, FileSystemContentRetriever};
use LanguageServer\Index\{DependenciesIndex, GlobalIndex, Index, ProjectIndex, StubsIndex}; use LanguageServer\Index\{DependenciesIndex, GlobalIndex, Index, ProjectIndex, StubsIndex};
use LanguageServer\Cache\{FileSystemCache, ClientCache}; use LanguageServer\Cache\{FileSystemCache, ClientCache};
use AdvancedJsonRpc; use AdvancedJsonRpc;
use Sabre\Event\{Loop, Promise}; use Sabre\Event\Promise;
use function Sabre\Event\coroutine; use function Sabre\Event\coroutine;
use Exception;
use Throwable; use Throwable;
use Webmozart\PathUtil\Path; use Webmozart\PathUtil\Path;
use Sabre\Uri;
class LanguageServer extends AdvancedJsonRpc\Dispatcher class LanguageServer extends AdvancedJsonRpc\Dispatcher
{ {
@ -111,7 +107,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
protected $definitionResolver; protected $definitionResolver;
/** /**
* @param PotocolReader $reader * @param ProtocolReader $reader
* @param ProtocolWriter $writer * @param ProtocolWriter $writer
*/ */
public function __construct(ProtocolReader $reader, ProtocolWriter $writer) public function __construct(ProtocolReader $reader, ProtocolWriter $writer)
@ -137,7 +133,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
// If a ResponseError is thrown, send it back in the Response // If a ResponseError is thrown, send it back in the Response
$error = $e; $error = $e;
} catch (Throwable $e) { } catch (Throwable $e) {
// If an unexpected error occured, send back an INTERNAL_ERROR error response // If an unexpected error occurred, send back an INTERNAL_ERROR error response
$error = new AdvancedJsonRpc\Error( $error = new AdvancedJsonRpc\Error(
(string)$e, (string)$e,
AdvancedJsonRpc\ErrorCode::INTERNAL_ERROR, AdvancedJsonRpc\ErrorCode::INTERNAL_ERROR,
@ -275,8 +271,6 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
$serverCapabilities->documentSymbolProvider = true; $serverCapabilities->documentSymbolProvider = true;
// Support "Find all symbols in workspace" // Support "Find all symbols in workspace"
$serverCapabilities->workspaceSymbolProvider = true; $serverCapabilities->workspaceSymbolProvider = true;
// Support "Format Code"
$serverCapabilities->documentFormattingProvider = true;
// Support "Go to definition" // Support "Go to definition"
$serverCapabilities->definitionProvider = true; $serverCapabilities->definitionProvider = true;
// Support "Find all references" // Support "Find all references"
@ -287,6 +281,10 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
$serverCapabilities->completionProvider = new CompletionOptions; $serverCapabilities->completionProvider = new CompletionOptions;
$serverCapabilities->completionProvider->resolveProvider = false; $serverCapabilities->completionProvider->resolveProvider = false;
$serverCapabilities->completionProvider->triggerCharacters = ['$', '>']; $serverCapabilities->completionProvider->triggerCharacters = ['$', '>'];
$serverCapabilities->signatureHelpProvider = new SignatureHelpOptions();
$serverCapabilities->signatureHelpProvider->triggerCharacters = ['(', ','];
// Support global references // Support global references
$serverCapabilities->xworkspaceReferencesProvider = true; $serverCapabilities->xworkspaceReferencesProvider = true;
$serverCapabilities->xdefinitionProvider = true; $serverCapabilities->xdefinitionProvider = true;

View File

@ -1,41 +0,0 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\NodeVisitor;
use PhpParser\{NodeVisitorAbstract, Node};
class ColumnCalculator extends NodeVisitorAbstract
{
private $code;
private $codeLength;
public function __construct($code)
{
$this->code = $code;
$this->codeLength = strlen($code);
}
public function enterNode(Node $node)
{
$startFilePos = $node->getAttribute('startFilePos');
$endFilePos = $node->getAttribute('endFilePos');
if ($startFilePos > $this->codeLength || $endFilePos > $this->codeLength) {
throw new \RuntimeException('Invalid position information');
}
$startLinePos = strrpos($this->code, "\n", $startFilePos - $this->codeLength);
if ($startLinePos === false) {
$startLinePos = -1;
}
$endLinePos = strrpos($this->code, "\n", $endFilePos - $this->codeLength);
if ($endLinePos === false) {
$endLinePos = -1;
}
$node->setAttribute('startColumn', $startFilePos - $startLinePos);
$node->setAttribute('endColumn', $endFilePos - $endLinePos);
}
}

View File

@ -1,47 +0,0 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\NodeVisitor;
use PhpParser\{NodeVisitorAbstract, Node};
use LanguageServer\{Definition, DefinitionResolver};
use LanguageServer\Protocol\SymbolInformation;
/**
* Collects definitions of classes, interfaces, traits, methods, properties and constants
* Depends on ReferencesAdder and NameResolver
*/
class DefinitionCollector extends NodeVisitorAbstract
{
/**
* Map from fully qualified name (FQN) to Definition
*
* @var Definition[]
*/
public $definitions = [];
/**
* Map from fully qualified name (FQN) to Node
*
* @var Node[]
*/
public $nodes = [];
private $definitionResolver;
public function __construct(DefinitionResolver $definitionResolver)
{
$this->definitionResolver = $definitionResolver;
}
public function enterNode(Node $node)
{
$fqn = DefinitionResolver::getDefinedFqn($node);
// Only index definitions with an FQN (no variables)
if ($fqn === null) {
return;
}
$this->nodes[$fqn] = $node;
$this->definitions[$fqn] = $this->definitionResolver->createDefinitionFromNode($node, $fqn);
}
}

View File

@ -1,96 +0,0 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\NodeVisitor;
use PhpParser;
use PhpParser\{NodeVisitorAbstract, Node, Comment};
use phpDocumentor\Reflection\DocBlockFactory;
use phpDocumentor\Reflection\Types\Context;
use Exception;
/**
* Decorates all nodes with a docBlock attribute that is an instance of phpDocumentor\Reflection\DocBlock
*/
class DocBlockParser extends NodeVisitorAbstract
{
/**
* @var DocBlockFactory
*/
private $docBlockFactory;
/**
* The current namespace context
*
* @var string
*/
private $namespace;
/**
* Prefix from a parent group use declaration
*
* @var string
*/
private $prefix;
/**
* Namespace aliases in the current context
*
* @var string[]
*/
private $aliases;
/**
* @var PhpParser\Error[]
*/
public $errors = [];
public function __construct(DocBlockFactory $docBlockFactory)
{
$this->docBlockFactory = $docBlockFactory;
}
public function beforeTraverse(array $nodes)
{
$this->namespace = '';
$this->prefix = '';
$this->aliases = [];
}
public function enterNode(Node $node)
{
if ($node instanceof Node\Stmt\Namespace_) {
$this->namespace = (string)$node->name;
} else if ($node instanceof Node\Stmt\GroupUse) {
$this->prefix = (string)$node->prefix . '\\';
} else if ($node instanceof Node\Stmt\UseUse) {
$this->aliases[$node->alias] = $this->prefix . (string)$node->name;
}
$docComment = $node->getDocComment();
if ($docComment === null) {
return;
}
$context = new Context($this->namespace, $this->aliases);
try {
$docBlock = $this->docBlockFactory->create($docComment->getText(), $context);
$node->setAttribute('docBlock', $docBlock);
} catch (Exception $e) {
$this->errors[] = new PhpParser\Error($e->getMessage(), [
'startFilePos' => $docComment->getFilePos(),
'endFilePos' => $docComment->getFilePos() + strlen($docComment->getText()),
'startLine' => $docComment->getLine(),
'endLine' => $docComment->getLine() + preg_match_all('/[\\n\\r]/', $docComment->getText()) + 1
]);
}
}
public function leaveNode(Node $node)
{
if ($node instanceof Node\Stmt\Namespace_) {
$this->namespace = '';
$this->aliases = [];
} else if ($node instanceof Node\Stmt\GroupUse) {
$this->prefix = '';
}
}
}

View File

@ -1,45 +0,0 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\NodeVisitor;
use PhpParser\{NodeVisitorAbstract, Node, NodeTraverser};
use LanguageServer\Protocol\{Position, Range};
/**
* Finds the Node at a specified position
* Depends on ColumnCalculator
*/
class NodeAtPositionFinder extends NodeVisitorAbstract
{
/**
* The node at the position, if found
*
* @var Node|null
*/
public $node;
/**
* @var Position
*/
private $position;
/**
* @param Position $position The position where the node is located
*/
public function __construct(Position $position)
{
$this->position = $position;
}
public function leaveNode(Node $node)
{
if ($this->node === null) {
$range = Range::fromNode($node);
if ($range->includes($this->position)) {
$this->node = $node;
return NodeTraverser::STOP_TRAVERSAL;
}
}
}
}

View File

@ -1,54 +0,0 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\NodeVisitor;
use PhpParser\{NodeVisitorAbstract, Node};
/**
* Decorates all nodes with parent and sibling references (similar to DOM nodes)
*/
class ReferencesAdder extends NodeVisitorAbstract
{
/**
* @var Node[]
*/
private $stack = [];
/**
* @var Node
*/
private $previous;
/**
* @var mixed
*/
private $document;
/**
* @param mixed $document The value for the ownerDocument attribute
*/
public function __construct($document = null)
{
$this->document = $document;
}
public function enterNode(Node $node)
{
$node->setAttribute('ownerDocument', $this->document);
if (!empty($this->stack)) {
$node->setAttribute('parentNode', end($this->stack));
}
if (isset($this->previous) && $this->previous->getAttribute('parentNode') === $node->getAttribute('parentNode')) {
$node->setAttribute('previousSibling', $this->previous);
$this->previous->setAttribute('nextSibling', $node);
}
$this->stack[] = $node;
}
public function leaveNode(Node $node)
{
$this->previous = $node;
array_pop($this->stack);
}
}

View File

@ -1,75 +0,0 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\NodeVisitor;
use PhpParser\{NodeVisitorAbstract, Node};
use LanguageServer\DefinitionResolver;
/**
* Collects references to classes, interfaces, traits, methods, properties and constants
* Depends on ReferencesAdder and NameResolver
*/
class ReferencesCollector extends NodeVisitorAbstract
{
/**
* Map from fully qualified name (FQN) to array of nodes that reference the symbol
*
* @var Node[][]
*/
public $nodes = [];
/**
* @var DefinitionResolver
*/
private $definitionResolver;
/**
* @param DefinitionResolver $definitionResolver The DefinitionResolver to resolve reference nodes to definitions
*/
public function __construct(DefinitionResolver $definitionResolver)
{
$this->definitionResolver = $definitionResolver;
}
public function enterNode(Node $node)
{
// Check if the node references any global symbol
$fqn = $this->definitionResolver->resolveReferenceNodeToFqn($node);
if ($fqn) {
$parent = $node->getAttribute('parentNode');
$grandParent = $parent ? $parent->getAttribute('parentNode') : null;
$this->addReference($fqn, $node);
if (
$node instanceof Node\Name
&& $node->isQualified()
&& !($parent instanceof Node\Stmt\Namespace_ && $parent->name === $node)
) {
// Add references for each referenced namespace
$ns = $fqn;
while (($pos = strrpos($ns, '\\')) !== false) {
$ns = substr($ns, 0, $pos);
$this->addReference($ns, $node);
}
}
// Namespaced constant access and function calls also need to register a reference
// to the global version because PHP falls back to global at runtime
// http://php.net/manual/en/language.namespaces.fallback.php
if ($parent instanceof Node\Expr\ConstFetch || $parent instanceof Node\Expr\FuncCall) {
$parts = explode('\\', $fqn);
if (count($parts) > 1) {
$globalFqn = end($parts);
$this->addReference($globalFqn, $node);
}
}
}
}
private function addReference(string $fqn, Node $node)
{
if (!isset($this->nodes[$fqn])) {
$this->nodes[$fqn] = [];
}
$this->nodes[$fqn][] = $node;
}
}

View File

@ -1,50 +0,0 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\NodeVisitor;
use PhpParser\{NodeVisitorAbstract, Node, NodeTraverser};
/**
* Collects all references to a variable
*/
class VariableReferencesCollector extends NodeVisitorAbstract
{
/**
* Array of references to the variable
*
* @var Node\Expr\Variable[]
*/
public $nodes = [];
/**
* @var string
*/
private $name;
/**
* @param string $name The variable name
*/
public function __construct(string $name)
{
$this->name = $name;
}
public function enterNode(Node $node)
{
if ($node instanceof Node\Expr\Variable && $node->name === $this->name) {
$this->nodes[] = $node;
} else if ($node instanceof Node\FunctionLike) {
// If we meet a function node, dont traverse its statements, they are in another scope
// except it is a closure that has imported the variable through use
if ($node instanceof Node\Expr\Closure) {
foreach ($node->uses as $use) {
if ($use->var === $this->name) {
return;
}
}
}
return NodeTraverser::DONT_TRAVERSE_CHILDREN;
}
}
}

View File

@ -1,25 +0,0 @@
<?php
namespace LanguageServer;
use PhpParser;
/**
* Custom PHP Parser class configured for our needs
*/
class Parser extends PhpParser\Parser\Php7
{
public function __construct()
{
$lexer = new PhpParser\Lexer([
'usedAttributes' => [
'comments',
'startLine',
'endLine',
'startFilePos',
'endFilePos'
]
]);
parent::__construct($lexer);
}
}

116
src/ParserHelpers.php Normal file
View File

@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace LanguageServer\ParserHelpers;
use Microsoft\PhpParser;
use Microsoft\PhpParser\Node;
function isConstantFetch(Node $node) : bool
{
$parent = $node->parent;
return
(
$node instanceof Node\QualifiedName &&
(
$parent instanceof Node\Expression ||
$parent instanceof Node\DelimitedList\ExpressionList ||
$parent instanceof Node\ArrayElement ||
($parent instanceof Node\Parameter && $node->parent->default === $node) ||
$parent instanceof Node\StatementNode ||
$parent instanceof Node\CaseStatementNode
) &&
!(
$parent instanceof Node\Expression\MemberAccessExpression ||
$parent instanceof Node\Expression\CallExpression ||
$parent instanceof Node\Expression\ObjectCreationExpression ||
$parent instanceof Node\Expression\ScopedPropertyAccessExpression ||
$parent instanceof PhpParser\FunctionLike ||
(
$parent instanceof Node\Expression\BinaryExpression &&
$parent->operator->kind === PhpParser\TokenKind::InstanceOfKeyword
)
));
}
function getFunctionLikeDeclarationFromParameter(Node\Parameter $node)
{
return $node->parent->parent;
}
function isBooleanExpression($expression) : bool
{
if (!($expression instanceof Node\Expression\BinaryExpression)) {
return false;
}
switch ($expression->operator->kind) {
case PhpParser\TokenKind::InstanceOfKeyword:
case PhpParser\TokenKind::GreaterThanToken:
case PhpParser\TokenKind::GreaterThanEqualsToken:
case PhpParser\TokenKind::LessThanToken:
case PhpParser\TokenKind::LessThanEqualsToken:
case PhpParser\TokenKind::AndKeyword:
case PhpParser\TokenKind::AmpersandAmpersandToken:
case PhpParser\TokenKind::LessThanEqualsGreaterThanToken:
case PhpParser\TokenKind::OrKeyword:
case PhpParser\TokenKind::BarBarToken:
case PhpParser\TokenKind::XorKeyword:
case PhpParser\TokenKind::ExclamationEqualsEqualsToken:
case PhpParser\TokenKind::ExclamationEqualsToken:
case PhpParser\TokenKind::CaretToken:
case PhpParser\TokenKind::EqualsEqualsEqualsToken:
case PhpParser\TokenKind::EqualsToken:
return true;
}
return false;
}
/**
* Tries to get the parent property declaration given a Node
* @param Node $node
* @return Node\PropertyDeclaration|null $node
*/
function tryGetPropertyDeclaration(Node $node)
{
if ($node instanceof Node\Expression\Variable &&
(($propertyDeclaration = $node->parent->parent) instanceof Node\PropertyDeclaration ||
($propertyDeclaration = $propertyDeclaration->parent) instanceof Node\PropertyDeclaration)
) {
return $propertyDeclaration;
}
return null;
}
/**
* Tries to get the parent ConstDeclaration or ClassConstDeclaration given a Node
* @param Node $node
* @return Node\Statement\ConstDeclaration|Node\ClassConstDeclaration|null $node
*/
function tryGetConstOrClassConstDeclaration(Node $node)
{
if (
$node instanceof Node\ConstElement && (
($constDeclaration = $node->parent->parent) instanceof Node\ClassConstDeclaration ||
$constDeclaration instanceof Node\Statement\ConstDeclaration )
) {
return $constDeclaration;
}
return null;
}
/**
* Returns true if the node is a usage of `define`.
* e.g. define('TEST_DEFINE_CONSTANT', false);
* @param Node $node
* @return bool
*/
function isConstDefineExpression(Node $node): bool
{
return $node instanceof Node\Expression\CallExpression
&& $node->callableExpression instanceof Node\QualifiedName
&& strtolower($node->callableExpression->getText()) === 'define'
&& isset($node->argumentExpressionList->children[0])
&& $node->argumentExpressionList->children[0]->expression instanceof Node\StringLiteral
&& isset($node->argumentExpressionList->children[2]);
}

View File

@ -3,27 +3,20 @@ declare(strict_types = 1);
namespace LanguageServer; namespace LanguageServer;
use LanguageServer\Protocol\{Diagnostic, DiagnosticSeverity, Range, Position, TextEdit};
use LanguageServer\NodeVisitor\{
NodeAtPositionFinder,
ReferencesAdder,
DocBlockParser,
DefinitionCollector,
ColumnCalculator,
ReferencesCollector
};
use LanguageServer\Index\Index; use LanguageServer\Index\Index;
use PhpParser\{Error, ErrorHandler, Node, NodeTraverser}; use LanguageServer\Protocol\{
use PhpParser\NodeVisitor\NameResolver; Diagnostic, Position, Range
};
use Microsoft\PhpParser;
use Microsoft\PhpParser\Node;
use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\DocBlockFactory;
use Sabre\Uri;
class PhpDocument class PhpDocument
{ {
/** /**
* The PHPParser instance * The PHPParser instance
* *
* @var Parser * @var PhpParser\Parser
*/ */
private $parser; private $parser;
@ -53,19 +46,12 @@ class PhpDocument
*/ */
private $uri; private $uri;
/**
* The content of the document
*
* @var string
*/
private $content;
/** /**
* The AST of the document * The AST of the document
* *
* @var Node[] * @var Node\SourceFileNode
*/ */
private $stmts; private $sourceFileNode;
/** /**
* Map from fully qualified name (FQN) to Definition * Map from fully qualified name (FQN) to Definition
@ -77,7 +63,7 @@ class PhpDocument
/** /**
* Map from fully qualified name (FQN) to Node * Map from fully qualified name (FQN) to Node
* *
* @var Node[] * @var Node
*/ */
private $definitionNodes; private $definitionNodes;
@ -96,18 +82,18 @@ class PhpDocument
private $diagnostics; private $diagnostics;
/** /**
* @param string $uri The URI of the document * @param string $uri The URI of the document
* @param string $content The content of the document * @param string $content The content of the document
* @param Index $index The Index to register definitions and references to * @param Index $index The Index to register definitions and references to
* @param Parser $parser The PHPParser instance * @param PhpParser\Parser $parser The PhpParser instance
* @param DocBlockFactory $docBlockFactory The DocBlockFactory instance to parse docblocks * @param DocBlockFactory $docBlockFactory The DocBlockFactory instance to parse docblocks
* @param DefinitionResolver $definitionResolver The DefinitionResolver to resolve definitions to symbols in the workspace * @param DefinitionResolver $definitionResolver The DefinitionResolver to resolve definitions to symbols in the workspace
*/ */
public function __construct( public function __construct(
string $uri, string $uri,
string $content, string $content,
Index $index, Index $index,
Parser $parser, $parser,
DocBlockFactory $docBlockFactory, DocBlockFactory $docBlockFactory,
DefinitionResolver $definitionResolver DefinitionResolver $definitionResolver
) { ) {
@ -133,15 +119,13 @@ class PhpDocument
/** /**
* Updates the content on this document. * Updates the content on this document.
* Re-parses a source file, updates symbols and reports parsing errors * Re-parses a source file, updates symbols and reports parsing errors
* that may have occured as diagnostics. * that may have occurred as diagnostics.
* *
* @param string $content * @param string $content
* @return void * @return void
*/ */
public function updateContent(string $content) public function updateContent(string $content)
{ {
$this->content = $content;
// Unregister old definitions // Unregister old definitions
if (isset($this->definitions)) { if (isset($this->definitions)) {
foreach ($this->definitions as $fqn => $definition) { foreach ($this->definitions as $fqn => $definition) {
@ -160,77 +144,28 @@ class PhpDocument
$this->definitions = null; $this->definitions = null;
$this->definitionNodes = null; $this->definitionNodes = null;
$errorHandler = new ErrorHandler\Collecting; $treeAnalyzer = new TreeAnalyzer($this->parser, $content, $this->docBlockFactory, $this->definitionResolver, $this->uri);
$stmts = $this->parser->parse($content, $errorHandler);
$this->diagnostics = []; $this->diagnostics = $treeAnalyzer->getDiagnostics();
foreach ($errorHandler->getErrors() as $error) {
$this->diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::ERROR, 'php'); $this->definitions = $treeAnalyzer->getDefinitions();
$this->definitionNodes = $treeAnalyzer->getDefinitionNodes();
$this->referenceNodes = $treeAnalyzer->getReferenceNodes();
foreach ($this->definitions as $fqn => $definition) {
$this->index->setDefinition($fqn, $definition);
} }
// $stmts can be null in case of a fatal parsing error // Register this document on the project for references
if ($stmts) { foreach ($this->referenceNodes as $fqn => $nodes) {
$traverser = new NodeTraverser; // Cast the key to string. If (string)'2' is set as an array index, it will read out as (int)2. We must
// deal with incorrect code, so this is a valid scenario.
// Resolve aliased names to FQNs $this->index->addReferenceUri((string)$fqn, $this->uri);
$traverser->addVisitor(new NameResolver($errorHandler));
// Add parentNode, previousSibling, nextSibling attributes
$traverser->addVisitor(new ReferencesAdder($this));
// Add column attributes to nodes
$traverser->addVisitor(new ColumnCalculator($content));
// Parse docblocks and add docBlock attributes to nodes
$docBlockParser = new DocBlockParser($this->docBlockFactory);
$traverser->addVisitor($docBlockParser);
$traverser->traverse($stmts);
// Report errors from parsing docblocks
foreach ($docBlockParser->errors as $error) {
$this->diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::WARNING, 'php');
}
$traverser = new NodeTraverser;
// Collect all definitions
$definitionCollector = new DefinitionCollector($this->definitionResolver);
$traverser->addVisitor($definitionCollector);
// Collect all references
$referencesCollector = new ReferencesCollector($this->definitionResolver);
$traverser->addVisitor($referencesCollector);
$traverser->traverse($stmts);
// Register this document on the project for all the symbols defined in it
$this->definitions = $definitionCollector->definitions;
$this->definitionNodes = $definitionCollector->nodes;
foreach ($definitionCollector->definitions as $fqn => $definition) {
$this->index->setDefinition($fqn, $definition);
}
// Register this document on the project for references
$this->referenceNodes = $referencesCollector->nodes;
foreach ($referencesCollector->nodes as $fqn => $nodes) {
$this->index->addReferenceUri($fqn, $this->uri);
}
$this->stmts = $stmts;
} }
}
/** $this->sourceFileNode = $treeAnalyzer->getSourceFileNode();
* Returns array of TextEdit changes to format this document.
*
* @return \LanguageServer\Protocol\TextEdit[]
*/
public function getFormattedText()
{
if (empty($this->content)) {
return [];
}
return Formatter::format($this->content, $this->uri);
} }
/** /**
@ -240,7 +175,7 @@ class PhpDocument
*/ */
public function getContent() public function getContent()
{ {
return $this->content; return $this->sourceFileNode->fileContents;
} }
/** /**
@ -266,11 +201,11 @@ class PhpDocument
/** /**
* Returns the AST of the document * Returns the AST of the document
* *
* @return Node[] * @return Node\SourceFileNode|null
*/ */
public function getStmts(): array public function getSourceFileNode()
{ {
return $this->stmts; return $this->sourceFileNode;
} }
/** /**
@ -281,14 +216,16 @@ class PhpDocument
*/ */
public function getNodeAtPosition(Position $position) public function getNodeAtPosition(Position $position)
{ {
if ($this->stmts === null) { if ($this->sourceFileNode === null) {
return null; return null;
} }
$traverser = new NodeTraverser;
$finder = new NodeAtPositionFinder($position); $offset = $position->toOffset($this->sourceFileNode->getFileContents());
$traverser->addVisitor($finder); $node = $this->sourceFileNode->getDescendantNodeAtPosition($offset);
$traverser->traverse($this->stmts); if ($node !== null && $node->getStart() > $offset) {
return $finder->node; return null;
}
return $node;
} }
/** /**
@ -299,12 +236,10 @@ class PhpDocument
*/ */
public function getRange(Range $range) public function getRange(Range $range)
{ {
if ($this->content === null) { $content = $this->getContent();
return null; $start = $range->start->toOffset($content);
} $length = $range->end->toOffset($content) - $start;
$start = $range->start->toOffset($this->content); return substr($content, $start, $length);
$length = $range->end->toOffset($this->content) - $start;
return substr($this->content, $start, $length);
} }
/** /**

View File

@ -8,6 +8,7 @@ use LanguageServer\Index\ProjectIndex;
use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\DocBlockFactory;
use Sabre\Event\Promise; use Sabre\Event\Promise;
use function Sabre\Event\coroutine; use function Sabre\Event\coroutine;
use Microsoft\PhpParser;
/** /**
* Takes care of loading documents and managing "open" documents * Takes care of loading documents and managing "open" documents
@ -36,6 +37,11 @@ class PhpDocumentLoader
*/ */
private $parser; private $parser;
/**
* @var PhpParser\Parser
*/
private $tolerantParser;
/** /**
* @var DocBlockFactory * @var DocBlockFactory
*/ */
@ -47,9 +53,10 @@ class PhpDocumentLoader
private $definitionResolver; private $definitionResolver;
/** /**
* @param ContentRetriever $contentRetriever * @param ContentRetriever $contentRetriever
* @param ProjectIndex $project * @param ProjectIndex $projectIndex
* @param DefinitionResolver $definitionResolver * @param DefinitionResolver $definitionResolver
* @internal param ProjectIndex $project
*/ */
public function __construct( public function __construct(
ContentRetriever $contentRetriever, ContentRetriever $contentRetriever,
@ -59,7 +66,7 @@ class PhpDocumentLoader
$this->contentRetriever = $contentRetriever; $this->contentRetriever = $contentRetriever;
$this->projectIndex = $projectIndex; $this->projectIndex = $projectIndex;
$this->definitionResolver = $definitionResolver; $this->definitionResolver = $definitionResolver;
$this->parser = new Parser; $this->parser = new PhpParser\Parser();
$this->docBlockFactory = DocBlockFactory::createInstance(); $this->docBlockFactory = DocBlockFactory::createInstance();
} }

View File

@ -0,0 +1,30 @@
<?php
namespace LanguageServer\Protocol;
/**
* Contains additional information about the context in which a completion request is triggered.
*/
class CompletionContext
{
/**
* How the completion was triggered.
*
* @var int
*/
public $triggerKind;
/**
* The trigger character (a single character) that has trigger code complete.
* Is null if `triggerKind !== CompletionTriggerKind::TRIGGER_CHARACTER`
*
* @var string|null
*/
public $triggerCharacter;
public function __construct(int $triggerKind = null, string $triggerCharacter = null)
{
$this->triggerKind = $triggerKind;
$this->triggerCharacter = $triggerCharacter;
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace LanguageServer\Protocol;
class CompletionTriggerKind
{
/**
* Completion was triggered by invoking it manuall or using API.
*/
const INVOKED = 1;
/**
* Completion was triggered by a trigger character.
*/
const TRIGGER_CHARACTER = 2;
}

View File

@ -2,8 +2,6 @@
namespace LanguageServer\Protocol; namespace LanguageServer\Protocol;
use PhpParser\Error;
/** /**
* Represents a diagnostic, such as a compiler error or warning. Diagnostic objects are only valid in the scope of a * Represents a diagnostic, such as a compiler error or warning. Diagnostic objects are only valid in the scope of a
* resource. * resource.
@ -47,26 +45,6 @@ class Diagnostic
*/ */
public $message; public $message;
/**
* Creates a diagnostic from a PhpParser Error
*
* @param Error $error Message and code will be used
* @param string $content The file content to calculate the column info
* @param int $severity DiagnosticSeverity
* @param string $source A human-readable string describing the source of this diagnostic
* @return self
*/
public static function fromError(Error $error, string $content, int $severity = null, string $source = null): self
{
return new self(
$error->getRawMessage(), // Do not include "on line ..." in the error message
Range::fromError($error, $content),
$error->getCode(),
$severity,
$source
);
}
/** /**
* @param string $message The diagnostic's message * @param string $message The diagnostic's message
* @param Range $range The range at which the message applies * @param Range $range The range at which the message applies

View File

@ -7,7 +7,7 @@ namespace LanguageServer\Protocol;
* special attention. Usually a document highlight is visualized by changing * special attention. Usually a document highlight is visualized by changing
* the background color of its range. * the background color of its range.
*/ */
class DocumentHighlightKind class DocumentHighlight
{ {
/** /**
* The range this highlight applies to. * The range this highlight applies to.

View File

@ -2,7 +2,8 @@
namespace LanguageServer\Protocol; namespace LanguageServer\Protocol;
use PhpParser\Node; use Microsoft\PhpParser;
use Microsoft\PhpParser\Node;
/** /**
* Represents a location inside a resource, such as a line inside a text file. * Represents a location inside a resource, such as a line inside a text file.
@ -25,9 +26,13 @@ class Location
* @param Node $node * @param Node $node
* @return self * @return self
*/ */
public static function fromNode(Node $node) public static function fromNode($node)
{ {
return new self($node->getAttribute('ownerDocument')->getUri(), Range::fromNode($node)); $range = PhpParser\PositionUtilities::getRangeFromPosition($node->getStart(), $node->getWidth(), $node->getFileContents());
return new self($node->getUri(), new Range(
new Position($range->start->line, $range->start->character),
new Position($range->end->line, $range->end->character)
));
} }
public function __construct(string $uri = null, Range $range = null) public function __construct(string $uri = null, Range $range = null)

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Protocol;
/**
* Uniquely identifies a Composer package
*/
class PackageDescriptor
{
/**
* The package name
*
* @var string
*/
public $name;
/**
* @param string $name The package name
*/
public function __construct(string $name = null)
{
$this->name = $name;
}
}

View File

@ -23,4 +23,17 @@ class ParameterInformation
* @var string|null * @var string|null
*/ */
public $documentation; public $documentation;
/**
* Create ParameterInformation
*
* @param string $label The label of this signature. Will be shown in the UI.
* @param string $documentation The human-readable doc-comment of this signature. Will be shown in the UI but can
* be omitted.
*/
public function __construct(string $label, string $documentation = null)
{
$this->label = $label;
$this->documentation = $documentation;
}
} }

View File

@ -2,7 +2,8 @@
namespace LanguageServer\Protocol; namespace LanguageServer\Protocol;
use PhpParser\{Error, Node}; use Microsoft\PhpParser;
use Microsoft\PhpParser\Node;
/** /**
* A range in a text document expressed as (zero-based) start and end positions. * A range in a text document expressed as (zero-based) start and end positions.
@ -31,26 +32,12 @@ class Range
*/ */
public static function fromNode(Node $node) public static function fromNode(Node $node)
{ {
return new self( $range = PhpParser\PositionUtilities::getRangeFromPosition($node->getStart(), $node->getWidth(), $node->getFileContents());
new Position($node->getAttribute('startLine') - 1, $node->getAttribute('startColumn') - 1),
new Position($node->getAttribute('endLine') - 1, $node->getAttribute('endColumn'))
);
}
/** return new self(
* Returns the range where an error occured new Position($range->start->line, $range->start->character),
* new Position($range->end->line, $range->end->character)
* @param \PhpParser\Error $error );
* @param string $content
* @return self
*/
public static function fromError(Error $error, string $content)
{
$startLine = max($error->getStartLine() - 1, 0);
$endLine = max($error->getEndLine() - 1, $startLine);
$startColumn = $error->hasColumnInfo() ? $error->getStartColumn($content) - 1 : 0;
$endColumn = $error->hasColumnInfo() ? $error->getEndColumn($content) : 0;
return new self(new Position($startLine, $startColumn), new Position($endLine, $endColumn));
} }
public function __construct(Position $start = null, Position $end = null) public function __construct(Position $start = null, Position $end = null)

View File

@ -29,4 +29,18 @@ class SignatureHelp
* @var int|null * @var int|null
*/ */
public $activeParameter; public $activeParameter;
/**
* Create a SignatureHelp
*
* @param SignatureInformation[] $signatures List of signature information
* @param int|null $activeSignature The active signature, zero based
* @param int|null $activeParameter The active parameter, zero based
*/
public function __construct(array $signatures = [], $activeSignature = null, int $activeParameter = null)
{
$this->signatures = $signatures;
$this->activeSignature = $activeSignature;
$this->activeParameter = $activeParameter;
}
} }

View File

@ -31,4 +31,19 @@ class SignatureInformation
* @var ParameterInformation[]|null * @var ParameterInformation[]|null
*/ */
public $parameters; public $parameters;
/**
* Create a SignatureInformation
*
* @param string $label The label of this signature. Will be shown in the UI.
* @param ParameterInformation[]|null The parameters of this signature
* @param string|null The human-readable doc-comment of this signature. Will be shown in the UI
* but can be omitted.
*/
public function __construct(string $label, array $parameters = null, string $documentation = null)
{
$this->label = $label;
$this->parameters = $parameters;
$this->documentation = $documentation;
}
} }

View File

@ -3,7 +3,10 @@ declare(strict_types = 1);
namespace LanguageServer\Protocol; namespace LanguageServer\Protocol;
class SymbolDescriptor extends SymbolInformation /**
* Uniquely identifies a symbol
*/
class SymbolDescriptor
{ {
/** /**
* The fully qualified structural element name, a globally unique identifier for the symbol. * The fully qualified structural element name, a globally unique identifier for the symbol.
@ -13,19 +16,17 @@ class SymbolDescriptor extends SymbolInformation
public $fqsen; public $fqsen;
/** /**
* A package from the composer.lock file or the contents of the composer.json * Identifies the Composer package the symbol is defined in (if any)
* Example: https://github.com/composer/composer/blob/master/composer.lock#L10
* Available fields may differ
* *
* @var object|null * @var PackageDescriptor|null
*/ */
public $package; public $package;
/** /**
* @param string $fqsen The fully qualified structural element name, a globally unique identifier for the symbol. * @param string $fqsen The fully qualified structural element name, a globally unique identifier for the symbol.
* @param object $package A package from the composer.lock file or the contents of the composer.json * @param PackageDescriptor $package Identifies the Composer package the symbol is defined in
*/ */
public function __construct(string $fqsen = null, $package = null) public function __construct(string $fqsen = null, PackageDescriptor $package = null)
{ {
$this->fqsen = $fqsen; $this->fqsen = $fqsen;
$this->package = $package; $this->package = $package;

View File

@ -2,8 +2,8 @@
namespace LanguageServer\Protocol; namespace LanguageServer\Protocol;
use PhpParser\Node; use Microsoft\PhpParser;
use Exception; use Microsoft\PhpParser\Node;
/** /**
* Represents information about programming constructs like variables, classes, * Represents information about programming constructs like variables, classes,
@ -44,51 +44,70 @@ class SymbolInformation
* *
* @param Node $node * @param Node $node
* @param string $fqn If given, $containerName will be extracted from it * @param string $fqn If given, $containerName will be extracted from it
* @return self|null * @return SymbolInformation|null
*/ */
public static function fromNode(Node $node, string $fqn = null) public static function fromNode($node, string $fqn = null)
{ {
$parent = $node->getAttribute('parentNode');
$symbol = new self; $symbol = new self;
if ($node instanceof Node\Stmt\Class_) { if ($node instanceof Node\Statement\ClassDeclaration) {
$symbol->kind = SymbolKind::CLASS_; $symbol->kind = SymbolKind::CLASS_;
} else if ($node instanceof Node\Stmt\Trait_) { } else if ($node instanceof Node\Statement\TraitDeclaration) {
$symbol->kind = SymbolKind::CLASS_; $symbol->kind = SymbolKind::CLASS_;
} else if ($node instanceof Node\Stmt\Interface_) { } else if (\LanguageServer\ParserHelpers\isConstDefineExpression($node)) {
// constants with define() like
// define('TEST_DEFINE_CONSTANT', false);
$symbol->kind = SymbolKind::CONSTANT;
$symbol->name = $node->argumentExpressionList->children[0]->expression->getStringContentsText();
} else if ($node instanceof Node\Statement\InterfaceDeclaration) {
$symbol->kind = SymbolKind::INTERFACE; $symbol->kind = SymbolKind::INTERFACE;
} else if ($node instanceof Node\Name && $parent instanceof Node\Stmt\Namespace_) { } else if ($node instanceof Node\Statement\NamespaceDefinition) {
$symbol->kind = SymbolKind::NAMESPACE; $symbol->kind = SymbolKind::NAMESPACE;
} else if ($node instanceof Node\Stmt\Function_) { } else if ($node instanceof Node\Statement\FunctionDeclaration) {
$symbol->kind = SymbolKind::FUNCTION; $symbol->kind = SymbolKind::FUNCTION;
} else if ($node instanceof Node\Stmt\ClassMethod) { } else if ($node instanceof Node\MethodDeclaration) {
$symbol->kind = SymbolKind::METHOD; $nameText = $node->getName();
} else if ($node instanceof Node\Stmt\PropertyProperty) { if ($nameText === '__construct' || $nameText === '__destruct') {
$symbol->kind = SymbolKind::CONSTRUCTOR;
} else {
$symbol->kind = SymbolKind::METHOD;
}
} else if ($node instanceof Node\Expression\Variable && $node->getFirstAncestor(Node\PropertyDeclaration::class) !== null) {
$symbol->kind = SymbolKind::PROPERTY; $symbol->kind = SymbolKind::PROPERTY;
} else if ($node instanceof Node\Const_) { } else if ($node instanceof Node\ConstElement) {
$symbol->kind = SymbolKind::CONSTANT; $symbol->kind = SymbolKind::CONSTANT;
} else if ( } else if (
( (
($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignOp) ($node instanceof Node\Expression\AssignmentExpression)
&& $node->var instanceof Node\Expr\Variable && $node->leftOperand instanceof Node\Expression\Variable
) )
|| $node instanceof Node\Expr\ClosureUse || $node instanceof Node\UseVariableName
|| $node instanceof Node\Param || $node instanceof Node\Parameter
) { ) {
$symbol->kind = SymbolKind::VARIABLE; $symbol->kind = SymbolKind::VARIABLE;
} else { } else {
return null; return null;
} }
if ($node instanceof Node\Name) {
$symbol->name = (string)$node; if ($node instanceof Node\Expression\AssignmentExpression) {
} else if ($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignOp) { if ($node->leftOperand instanceof Node\Expression\Variable) {
$symbol->name = $node->var->name; $symbol->name = $node->leftOperand->getName();
} else if ($node instanceof Node\Expr\ClosureUse) { } elseif ($node->leftOperand instanceof PhpParser\Token) {
$symbol->name = $node->var; $symbol->name = trim($node->leftOperand->getText($node->getFileContents()), "$");
}
} else if ($node instanceof Node\UseVariableName) {
$symbol->name = $node->getName();
} else if (isset($node->name)) { } else if (isset($node->name)) {
$symbol->name = (string)$node->name; if ($node->name instanceof Node\QualifiedName) {
} else { $symbol->name = (string)PhpParser\ResolvedName::buildName($node->name->nameParts, $node->getFileContents());
} else {
$symbol->name = ltrim((string)$node->name->getText($node->getFileContents()), "$");
}
} else if (isset($node->variableName)) {
$symbol->name = $node->variableName->getText($node);
} else if (!isset($symbol->name)) {
return null; return null;
} }
$symbol->location = Location::fromNode($node); $symbol->location = Location::fromNode($node);
if ($fqn !== null) { if ($fqn !== null) {
$parts = preg_split('/(::|->|\\\\)/', $fqn); $parts = preg_split('/(::|->|\\\\)/', $fqn);

View File

@ -8,7 +8,6 @@ use Sabre\Event\{
Loop, Loop,
Promise Promise
}; };
use RuntimeException;
class ProtocolStreamWriter implements ProtocolWriter class ProtocolStreamWriter implements ProtocolWriter
{ {

View File

@ -3,34 +3,33 @@ declare(strict_types = 1);
namespace LanguageServer\Server; namespace LanguageServer\Server;
use PhpParser\PrettyPrinter\Standard as PrettyPrinter; use LanguageServer\{
use PhpParser\{Node, NodeTraverser}; CompletionProvider, SignatureHelpProvider, LanguageClient, PhpDocument, PhpDocumentLoader, DefinitionResolver
use LanguageServer\{LanguageClient, PhpDocumentLoader, PhpDocument, DefinitionResolver, CompletionProvider};
use LanguageServer\NodeVisitor\VariableReferencesCollector;
use LanguageServer\Protocol\{
SymbolLocationInformation,
SymbolDescriptor,
TextDocumentItem,
TextDocumentIdentifier,
VersionedTextDocumentIdentifier,
Position,
Range,
FormattingOptions,
TextEdit,
Location,
SymbolInformation,
ReferenceContext,
Hover,
MarkedString,
SymbolKind,
CompletionItem,
CompletionItemKind
}; };
use LanguageServer\Index\ReadableIndex; use LanguageServer\Index\ReadableIndex;
use LanguageServer\Protocol\{
FormattingOptions,
Hover,
Location,
MarkedString,
Position,
Range,
ReferenceContext,
SymbolDescriptor,
PackageDescriptor,
SymbolLocationInformation,
TextDocumentIdentifier,
TextDocumentItem,
VersionedTextDocumentIdentifier,
CompletionContext
};
use Microsoft\PhpParser\Node;
use Sabre\Event\Promise; use Sabre\Event\Promise;
use Sabre\Uri; use Sabre\Uri;
use function LanguageServer\{
isVendored, waitForEvent, getPackageName
};
use function Sabre\Event\coroutine; use function Sabre\Event\coroutine;
use function LanguageServer\{waitForEvent, isVendored};
/** /**
* Provides method handlers for all textDocument/* methods * Provides method handlers for all textDocument/* methods
@ -49,11 +48,6 @@ class TextDocument
*/ */
protected $project; protected $project;
/**
* @var PrettyPrinter
*/
protected $prettyPrinter;
/** /**
* @var DefinitionResolver * @var DefinitionResolver
*/ */
@ -64,6 +58,11 @@ class TextDocument
*/ */
protected $completionProvider; protected $completionProvider;
/**
* @var SignatureHelpProvider
*/
protected $signatureHelpProvider;
/** /**
* @var ReadableIndex * @var ReadableIndex
*/ */
@ -80,12 +79,12 @@ class TextDocument
protected $composerLock; protected $composerLock;
/** /**
* @param PhpDocumentLoader $documentLoader * @param PhpDocumentLoader $documentLoader
* @param DefinitionResolver $definitionResolver * @param DefinitionResolver $definitionResolver
* @param LanguageClient $client * @param LanguageClient $client
* @param ReadableIndex $index * @param ReadableIndex $index
* @param \stdClass $composerJson * @param \stdClass $composerJson
* @param \stdClass $composerLock * @param \stdClass $composerLock
*/ */
public function __construct( public function __construct(
PhpDocumentLoader $documentLoader, PhpDocumentLoader $documentLoader,
@ -97,9 +96,9 @@ class TextDocument
) { ) {
$this->documentLoader = $documentLoader; $this->documentLoader = $documentLoader;
$this->client = $client; $this->client = $client;
$this->prettyPrinter = new PrettyPrinter();
$this->definitionResolver = $definitionResolver; $this->definitionResolver = $definitionResolver;
$this->completionProvider = new CompletionProvider($this->definitionResolver, $index); $this->completionProvider = new CompletionProvider($this->definitionResolver, $index);
$this->signatureHelpProvider = new SignatureHelpProvider($this->definitionResolver, $index, $documentLoader);
$this->index = $index; $this->index = $index;
$this->composerJson = $composerJson; $this->composerJson = $composerJson;
$this->composerLock = $composerLock; $this->composerLock = $composerLock;
@ -158,7 +157,7 @@ class TextDocument
* The document's truth now exists where the document's uri points to (e.g. if the document's uri is a file uri the * The document's truth now exists where the document's uri points to (e.g. if the document's uri is a file uri the
* truth now exists on disk). * truth now exists on disk).
* *
* @param \LanguageServer\Protocol\TextDocumentItem $textDocument The document that was closed * @param \LanguageServer\Protocol\TextDocumentIdentifier $textDocument The document that was closed
* @return void * @return void
*/ */
public function didClose(TextDocumentIdentifier $textDocument) public function didClose(TextDocumentIdentifier $textDocument)
@ -166,20 +165,6 @@ class TextDocument
$this->documentLoader->close($textDocument->uri); $this->documentLoader->close($textDocument->uri);
} }
/**
* The document formatting request is sent from the server to the client to format a whole document.
*
* @param TextDocumentIdentifier $textDocument The document to format
* @param FormattingOptions $options The format options
* @return Promise <TextEdit[]>
*/
public function formatting(TextDocumentIdentifier $textDocument, FormattingOptions $options)
{
return $this->documentLoader->getOrLoad($textDocument->uri)->then(function (PhpDocument $document) {
return $document->getFormattedText();
});
}
/** /**
* The references request is sent from the client to the server to resolve project-wide references for the symbol * The references request is sent from the client to the server to resolve project-wide references for the symbol
* denoted by the given text document position. * denoted by the given text document position.
@ -202,31 +187,34 @@ class TextDocument
// Variables always stay in the boundary of the file and need to be searched inside their function scope // Variables always stay in the boundary of the file and need to be searched inside their function scope
// by traversing the AST // by traversing the AST
if ( if (
$node instanceof Node\Expr\Variable
|| $node instanceof Node\Param ($node instanceof Node\Expression\Variable && !($node->getParent()->getParent() instanceof Node\PropertyDeclaration))
|| $node instanceof Node\Expr\ClosureUse || $node instanceof Node\Parameter
|| $node instanceof Node\UseVariableName
) { ) {
if ($node->name instanceof Node\Expr) { if (isset($node->name) && $node->name instanceof Node\Expression) {
return null; return null;
} }
// Find function/method/closure scope // Find function/method/closure scope
$n = $node; $n = $node;
while (isset($n) && !($n instanceof Node\FunctionLike)) {
$n = $n->getAttribute('parentNode'); $n = $n->getFirstAncestor(Node\Statement\FunctionDeclaration::class, Node\MethodDeclaration::class, Node\Expression\AnonymousFunctionCreationExpression::class, Node\SourceFileNode::class);
if ($n === null) {
$n = $node->getFirstAncestor(Node\Statement\ExpressionStatement::class)->getParent();
} }
if (!isset($n)) {
$n = $node->getAttribute('ownerDocument'); foreach ($n->getDescendantNodes() as $descendantNode) {
} if ($descendantNode instanceof Node\Expression\Variable &&
$traverser = new NodeTraverser; $descendantNode->getName() === $node->getName()
$refCollector = new VariableReferencesCollector($node->name); ) {
$traverser->addVisitor($refCollector); $locations[] = Location::fromNode($descendantNode);
$traverser->traverse($n->getStmts()); }
foreach ($refCollector->nodes as $ref) {
$locations[] = Location::fromNode($ref);
} }
} else { } else {
// Definition with a global FQN // Definition with a global FQN
$fqn = DefinitionResolver::getDefinedFqn($node); $fqn = DefinitionResolver::getDefinedFqn($node);
// Wait until indexing finished // Wait until indexing finished
if (!$this->index->isComplete()) { if (!$this->index->isComplete()) {
yield waitForEvent($this->index, 'complete'); yield waitForEvent($this->index, 'complete');
@ -254,6 +242,23 @@ class TextDocument
}); });
} }
/**
* The signature help request is sent from the client to the server to request signature information at a given
* cursor position.
*
* @param TextDocumentIdentifier $textDocument The text document
* @param Position $position The position inside the text document
*
* @return Promise <SignatureHelp>
*/
public function signatureHelp(TextDocumentIdentifier $textDocument, Position $position): Promise
{
return coroutine(function () use ($textDocument, $position) {
$document = yield $this->documentLoader->getOrLoad($textDocument->uri);
return $this->signatureHelpProvider->getSignatureHelp($document, $position);
});
}
/** /**
* The goto definition request is sent from the client to the server to resolve the definition location of a symbol * The goto definition request is sent from the client to the server to resolve the definition location of a symbol
* at a given text document position. * at a given text document position.
@ -331,6 +336,7 @@ class TextDocument
if ($def === null) { if ($def === null) {
return new Hover([], $range); return new Hover([], $range);
} }
$contents = [];
if ($def->declarationLine) { if ($def->declarationLine) {
$contents[] = new MarkedString('php', "<?php\n" . $def->declarationLine); $contents[] = new MarkedString('php', "<?php\n" . $def->declarationLine);
} }
@ -353,13 +359,14 @@ class TextDocument
* *
* @param TextDocumentIdentifier The text document * @param TextDocumentIdentifier The text document
* @param Position $position The position * @param Position $position The position
* @param CompletionContext|null $context The completion context
* @return Promise <CompletionItem[]|CompletionList> * @return Promise <CompletionItem[]|CompletionList>
*/ */
public function completion(TextDocumentIdentifier $textDocument, Position $position): Promise public function completion(TextDocumentIdentifier $textDocument, Position $position, CompletionContext $context = null): Promise
{ {
return coroutine(function () use ($textDocument, $position) { return coroutine(function () use ($textDocument, $position, $context) {
$document = yield $this->documentLoader->getOrLoad($textDocument->uri); $document = yield $this->documentLoader->getOrLoad($textDocument->uri);
return $this->completionProvider->provideCompletion($document, $position); return $this->completionProvider->provideCompletion($document, $position, $context);
}); });
} }
@ -384,9 +391,10 @@ class TextDocument
return []; return [];
} }
// Handle definition nodes // Handle definition nodes
$fqn = DefinitionResolver::getDefinedFqn($node);
while (true) { while (true) {
if ($fqn) { if ($fqn) {
$def = $this->index->getDefinition($definedFqn); $def = $this->index->getDefinition($fqn);
} else { } else {
// Handle reference nodes // Handle reference nodes
$def = $this->definitionResolver->resolveReferenceNodeToDefinition($node); $def = $this->definitionResolver->resolveReferenceNodeToDefinition($node);
@ -404,25 +412,14 @@ class TextDocument
) { ) {
return []; return [];
} }
$symbol = new SymbolDescriptor; // if Definition is inside a dependency, use the package name
foreach (get_object_vars($def->symbolInformation) as $prop => $val) {
$symbol->$prop = $val;
}
$symbol->fqsen = $def->fqn;
$packageName = getPackageName($def->symbolInformation->location->uri, $this->composerJson); $packageName = getPackageName($def->symbolInformation->location->uri, $this->composerJson);
if ($packageName && $this->composerLock !== null) { // else use the package name of the root package (if exists)
// Definition is inside a dependency if (!$packageName && $this->composerJson !== null) {
foreach (array_merge($this->composerLock->packages, $this->composerLock->{'packages-dev'}) as $package) { $packageName = $this->composerJson->name;
if ($package->name === $packageName) {
$symbol->package = $package;
break;
}
}
} else if ($this->composerJson !== null) {
// Definition belongs to a root package
$symbol->package = $this->composerJson;
} }
return [new SymbolLocationInformation($symbol, $symbol->location)]; $descriptor = new SymbolDescriptor($def->fqn, new PackageDescriptor($packageName));
return [new SymbolLocationInformation($descriptor, $def->symbolInformation->location)];
}); });
} }
} }

View File

@ -3,7 +3,7 @@ declare(strict_types = 1);
namespace LanguageServer\Server; namespace LanguageServer\Server;
use LanguageServer\{LanguageClient, Project, PhpDocumentLoader, Options, Indexer}; use LanguageServer\{LanguageClient, PhpDocumentLoader};
use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index}; use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index};
use LanguageServer\Protocol\{ use LanguageServer\Protocol\{
FileChangeType, FileChangeType,
@ -16,7 +16,7 @@ use LanguageServer\Protocol\{
}; };
use Sabre\Event\Promise; use Sabre\Event\Promise;
use function Sabre\Event\coroutine; use function Sabre\Event\coroutine;
use function LanguageServer\{waitForEvent, getPackageName}; use function LanguageServer\waitForEvent;
/** /**
* Provides method handlers for all workspace/* methods * Provides method handlers for all workspace/* methods
@ -33,7 +33,7 @@ class Workspace
* *
* @var ProjectIndex * @var ProjectIndex
*/ */
private $index; private $projectIndex;
/** /**
* @var DependenciesIndex * @var DependenciesIndex
@ -45,16 +45,6 @@ class Workspace
*/ */
private $sourceIndex; private $sourceIndex;
/**
* @var Options
*/
private $options;
/**
* @var Indexer
*/
private $indexer;
/** /**
* @var \stdClass * @var \stdClass
*/ */
@ -67,25 +57,21 @@ class Workspace
/** /**
* @param LanguageClient $client LanguageClient instance used to signal updated results * @param LanguageClient $client LanguageClient instance used to signal updated results
* @param ProjectIndex $index Index that is searched on a workspace/symbol request * @param ProjectIndex $projectIndex Index that is used to wait for full index completeness
* @param DependenciesIndex $dependenciesIndex Index that is used on a workspace/xreferences request * @param DependenciesIndex $dependenciesIndex Index that is used on a workspace/xreferences request
* @param DependenciesIndex $sourceIndex Index that is used on a workspace/xreferences request * @param DependenciesIndex $sourceIndex Index that is used on a workspace/xreferences request
* @param \stdClass $composerLock The parsed composer.lock of the project, if any * @param \stdClass $composerLock The parsed composer.lock of the project, if any
* @param PhpDocumentLoader $documentLoader PhpDocumentLoader instance to load documents * @param PhpDocumentLoader $documentLoader PhpDocumentLoader instance to load documents
* @param Indexer $indexer
* @param Options $options
*/ */
public function __construct(LanguageClient $client, ProjectIndex $index, DependenciesIndex $dependenciesIndex, Index $sourceIndex, \stdClass $composerLock = null, PhpDocumentLoader $documentLoader, \stdClass $composerJson = null, Indexer $indexer = null, Options $options = null) public function __construct(LanguageClient $client, ProjectIndex $projectIndex, DependenciesIndex $dependenciesIndex, Index $sourceIndex, \stdClass $composerLock = null, PhpDocumentLoader $documentLoader, \stdClass $composerJson = null)
{ {
$this->client = $client; $this->client = $client;
$this->sourceIndex = $sourceIndex; $this->sourceIndex = $sourceIndex;
$this->index = $index; $this->projectIndex = $projectIndex;
$this->dependenciesIndex = $dependenciesIndex; $this->dependenciesIndex = $dependenciesIndex;
$this->composerLock = $composerLock; $this->composerLock = $composerLock;
$this->documentLoader = $documentLoader; $this->documentLoader = $documentLoader;
$this->composerJson = $composerJson; $this->composerJson = $composerJson;
$this->indexer = $indexer;
$this->options = $options;
} }
/** /**
@ -98,11 +84,11 @@ class Workspace
{ {
return coroutine(function () use ($query) { return coroutine(function () use ($query) {
// Wait until indexing for definitions finished // Wait until indexing for definitions finished
if (!$this->index->isStaticComplete()) { if (!$this->sourceIndex->isStaticComplete()) {
yield waitForEvent($this->index, 'static-complete'); yield waitForEvent($this->sourceIndex, 'static-complete');
} }
$symbols = []; $symbols = [];
foreach ($this->index->getDefinitions() as $fqn => $definition) { foreach ($this->sourceIndex->getDefinitions() as $fqn => $definition) {
if ($query === '' || stripos($fqn, $query) !== false) { if ($query === '' || stripos($fqn, $query) !== false) {
$symbols[] = $definition->symbolInformation; $symbols[] = $definition->symbolInformation;
} }
@ -135,13 +121,14 @@ class Workspace
*/ */
public function xreferences($query, array $files = null): Promise public function xreferences($query, array $files = null): Promise
{ {
// TODO: $files is unused in the coroutine
return coroutine(function () use ($query, $files) { return coroutine(function () use ($query, $files) {
if ($this->composerLock === null) { if ($this->composerLock === null) {
return []; return [];
} }
// Wait until indexing finished // Wait until indexing finished
if (!$this->index->isComplete()) { if (!$this->projectIndex->isComplete()) {
yield waitForEvent($this->index, 'complete'); yield waitForEvent($this->projectIndex, 'complete');
} }
/** Map from URI to array of referenced FQNs in dependencies */ /** Map from URI to array of referenced FQNs in dependencies */
$refs = []; $refs = [];
@ -160,38 +147,11 @@ class Workspace
$refInfos = []; $refInfos = [];
foreach ($refs as $uri => $fqns) { foreach ($refs as $uri => $fqns) {
foreach ($fqns as $fqn) { foreach ($fqns as $fqn) {
$def = $this->dependenciesIndex->getDefinition($fqn);
$symbol = new SymbolDescriptor;
$symbol->fqsen = $fqn;
foreach (get_object_vars($def->symbolInformation) as $prop => $val) {
$symbol->$prop = $val;
}
// Find out package name
$packageName = getPackageName($def->symbolInformation->location->uri, $this->composerJson);
foreach (array_merge($this->composerLock->packages, $this->composerLock->{'packages-dev'}) as $package) {
if ($package->name === $packageName) {
$symbol->package = $package;
break;
}
}
// If there was no FQSEN provided, check if query attributes match
if (!isset($query->fqsen)) {
$matches = true;
foreach (get_object_vars($query) as $prop => $val) {
if ($query->$prop != $symbol->$prop) {
$matches = false;
break;
}
}
if (!$matches) {
continue;
}
}
$doc = yield $this->documentLoader->getOrLoad($uri); $doc = yield $this->documentLoader->getOrLoad($uri);
foreach ($doc->getReferenceNodesByFqn($fqn) as $node) { foreach ($doc->getReferenceNodesByFqn($fqn) as $node) {
$refInfo = new ReferenceInformation; $refInfo = new ReferenceInformation;
$refInfo->reference = Location::fromNode($node); $refInfo->reference = Location::fromNode($node);
$refInfo->symbol = $symbol; $refInfo->symbol = $query;
$refInfos[] = $refInfo; $refInfos[] = $refInfo;
} }
} }
@ -209,80 +169,9 @@ class Workspace
return []; return [];
} }
$dependencyReferences = []; $dependencyReferences = [];
foreach (array_merge($this->composerLock->packages, $this->composerLock->{'packages-dev'}) as $package) { foreach (array_merge($this->composerLock->packages, (array)$this->composerLock->{'packages-dev'}) as $package) {
$dependencyReferences[] = new DependencyReference($package); $dependencyReferences[] = new DependencyReference($package);
} }
return $dependencyReferences; return $dependencyReferences;
} }
/**
* A notification sent from the client to the server to signal the change of configuration settings.
*
* The default paramter type is Options but it also accepts different types
* which will be transformed on demand.
*
* Currently only the vscode format is supported
*
* @param mixed|null $settings
* @return bool
* @throws \Exception Settings format not valid
*/
public function didChangeConfiguration($settings = null): bool
{
// List of options that affect the indexer
$indexerOptions = ['fileTypes'];
if ($settings === null) {
return false;
}
// VSC sends the settings with the config section as main key
if ($settings instanceof \stdClass && $settings->phpIntelliSense) {
$mapper = new \JsonMapper();
$settings = $mapper->map($settings->phpIntelliSense, new Options);
}
if (!($settings instanceof Options)) {
throw new \Exception('Settings format not valid.');
}
$changedOptions = $this->getChangedOptions($settings);
if (empty($changedOptions)) {
return false;
}
foreach (get_object_vars($settings) as $prop => $val) {
$this->options->$prop = $val;
}
if ($this->indexer && !empty(array_intersect($changedOptions, $indexerOptions))) {
// check list of options that changed since last time against the list of valid indexer options
// wipe main index and start reindexing
$this->index->wipe();
$this->indexer->index()->otherwise('\\LanguageServer\\crash');
}
return true;
}
/**
* Get a list with all options that changed since last time
*
* @param Options $settings
* @return array List with changed options
*/
private function getChangedOptions(Options $settings): array
{
$old = get_object_vars($this->options);
$new = get_object_vars($settings);
$changed = array_udiff($old, $new, function ($a, $b) {
// custom callback since array_diff uses strings for comparison
return $a <=> $b;
});
return array_keys($changed);
}
} }

View File

@ -0,0 +1,187 @@
<?php
declare(strict_types = 1);
namespace LanguageServer;
use LanguageServer\Index\ReadableIndex;
use LanguageServer\Protocol\{
Position,
SignatureHelp,
ParameterInformation
};
use Microsoft\PhpParser\Node;
use Sabre\Event\Promise;
use function Sabre\Event\coroutine;
class SignatureHelpProvider
{
/** @var DefinitionResolver */
private $definitionResolver;
/** @var ReadableIndex */
private $index;
/** @var PhpDocumentLoader */
private $documentLoader;
/**
* Constructor
*
* @param DefinitionResolver $definitionResolver
* @param ReadableIndex $index
* @param PhpDocumentLoader $documentLoader
*/
public function __construct(DefinitionResolver $definitionResolver, ReadableIndex $index, PhpDocumentLoader $documentLoader)
{
$this->definitionResolver = $definitionResolver;
$this->index = $index;
$this->documentLoader = $documentLoader;
}
/**
* Finds signature help for a callable position
*
* @param PhpDocument $doc The document the position belongs to
* @param Position $position The position to detect a call from
*
* @return Promise <SignatureHelp>
*/
public function getSignatureHelp(PhpDocument $doc, Position $position): Promise
{
return coroutine(function () use ($doc, $position) {
// Find the node under the cursor
$node = $doc->getNodeAtPosition($position);
// Find the definition of the item being called
list($def, $argumentExpressionList) = yield $this->getCallingInfo($node);
if (!$def || !$def->signatureInformation) {
return new SignatureHelp();
}
// Find the active parameter
$activeParam = $argumentExpressionList
? $this->findActiveParameter($argumentExpressionList, $position, $doc)
: 0;
return new SignatureHelp([$def->signatureInformation], 0, $activeParam);
});
}
/**
* Given a node that could be a callable, finds the definition of the call and the argument expression list of
* the node
*
* @param Node $node The node to find calling information from
*
* @return Promise <array|null>
*/
private function getCallingInfo(Node $node)
{
return coroutine(function () use ($node) {
$fqn = null;
$callingNode = null;
if ($node instanceof Node\DelimitedList\ArgumentExpressionList) {
// Cursor is already inside a (
$argumentExpressionList = $node;
if ($node->parent instanceof Node\Expression\ObjectCreationExpression) {
// Constructing something
$callingNode = $node->parent->classTypeDesignator;
if (!$callingNode instanceof Node\QualifiedName) {
// We only support constructing from a QualifiedName
return null;
}
$fqn = $this->definitionResolver->resolveReferenceNodeToFqn($callingNode);
$fqn = "{$fqn}->__construct()";
} else {
$callingNode = $node->parent->getFirstChildNode(
Node\Expression\MemberAccessExpression::class,
Node\Expression\ScopedPropertyAccessExpression::class,
Node\QualifiedName::class
);
}
} elseif ($node instanceof Node\Expression\CallExpression) {
$argumentExpressionList = $node->getFirstChildNode(Node\DelimitedList\ArgumentExpressionList::class);
$callingNode = $node->getFirstChildNode(
Node\Expression\MemberAccessExpression::class,
Node\Expression\ScopedPropertyAccessExpression::class,
Node\QualifiedName::class
);
} elseif ($node instanceof Node\Expression\ObjectCreationExpression) {
$argumentExpressionList = $node->getFirstChildNode(Node\DelimitedList\ArgumentExpressionList::class);
$callingNode = $node->classTypeDesignator;
if (!$callingNode instanceof Node\QualifiedName) {
// We only support constructing from a QualifiedName
return null;
}
// Manually build the __construct fqn
$fqn = $this->definitionResolver->resolveReferenceNodeToFqn($callingNode);
$fqn = "{$fqn}->__construct()";
}
if (!$callingNode) {
return null;
}
// Now find the definition of the call
$fqn = $fqn ?: DefinitionResolver::getDefinedFqn($callingNode);
while (true) {
if ($fqn) {
$def = $this->index->getDefinition($fqn);
} else {
$def = $this->definitionResolver->resolveReferenceNodeToDefinition($callingNode);
}
if ($def !== null || $this->index->isComplete()) {
break;
}
yield waitForEvent($this->index, 'definition-added');
}
if (!$def) {
return null;
}
return [$def, $argumentExpressionList];
});
}
/**
* Given a position and arguments, finds the "active" argument at the position
*
* @param Node\DelimitedList\ArgumentExpressionList $argumentExpressionList The argument expression list
* @param Position $position The position to detect the active argument from
* @param PhpDocument $doc The document that contains the expression
*
* @return int
*/
private function findActiveParameter(
Node\DelimitedList\ArgumentExpressionList $argumentExpressionList,
Position $position,
PhpDocument $doc
): int {
$args = $argumentExpressionList->children;
$i = 0;
$found = null;
foreach ($args as $arg) {
if ($arg instanceof Node) {
$start = $arg->getFullStart();
$end = $arg->getEndPosition();
} else {
$start = $arg->fullStart;
$end = $start + $arg->length;
}
$offset = $position->toOffset($doc->getContent());
if ($offset >= $start && $offset <= $end) {
$found = $i;
break;
}
if ($arg instanceof Node) {
++$i;
}
}
if ($found === null) {
$found = $i;
}
return $found;
}
}

View File

@ -0,0 +1,91 @@
<?php
declare(strict_types = 1);
namespace LanguageServer;
use LanguageServer\Protocol\{SignatureInformation, ParameterInformation};
use Microsoft\PhpParser\FunctionLike;
class SignatureInformationFactory
{
/** @var DefinitionResolver */
private $definitionResolver;
/**
* Create a SignatureInformationFactory
*
* @param DefinitionResolver $definitionResolver
*/
public function __construct(DefinitionResolver $definitionResolver)
{
$this->definitionResolver = $definitionResolver;
}
/**
* Create a SignatureInformation from a FunctionLike node
*
* @param FunctionLike $node Node to create signature information from
*
* @return SignatureInformation
*/
public function create(FunctionLike $node): SignatureInformation
{
$params = $this->createParameters($node);
$label = $this->createLabel($params);
return new SignatureInformation(
$label,
$params,
$this->definitionResolver->getDocumentationFromNode($node)
);
}
/**
* Gets parameters from a FunctionLike node
*
* @param FunctionLike $node Node to get parameters from
*
* @return ParameterInformation[]
*/
private function createParameters(FunctionLike $node): array
{
$params = [];
if ($node->parameters) {
foreach ($node->parameters->getElements() as $element) {
$param = (string) $this->definitionResolver->getTypeFromNode($element);
$param .= ' ';
if ($element->dotDotDotToken) {
$param .= '...';
}
$param .= '$' . $element->getName();
if ($element->default) {
$param .= ' = ' . $element->default->getText();
}
$params[] = new ParameterInformation(
$param,
$this->definitionResolver->getDocumentationFromNode($element)
);
}
}
return $params;
}
/**
* Creates a signature information label from parameters
*
* @param ParameterInformation[] $params Parameters to create the label from
*
* @return string
*/
private function createLabel(array $params): string
{
$label = '(';
if ($params) {
foreach ($params as $param) {
$label .= $param->label . ', ';
}
$label = substr($label, 0, -2);
}
$label .= ')';
return $label;
}
}

25
src/StderrLogger.php Normal file
View File

@ -0,0 +1,25 @@
<?php
declare(strict_types = 1);
namespace LanguageServer;
/**
* Simple Logger that logs to STDERR
*/
class StderrLogger extends \Psr\Log\AbstractLogger implements \Psr\Log\LoggerInterface
{
/**
* Logs with an arbitrary level.
*
* @param mixed $level
* @param string $message
* @param array $context
*
* @return void
*/
public function log($level, $message, array $context = array())
{
$contextStr = empty($context) ? '' : ' ' . \json_encode($context, \JSON_UNESCAPED_SLASHES);
\fwrite(\STDERR, \str_pad(\strtoupper((string)$level), 10) . $message . $contextStr . \PHP_EOL);
}
}

286
src/TreeAnalyzer.php Normal file
View File

@ -0,0 +1,286 @@
<?php
declare(strict_types = 1);
namespace LanguageServer;
use LanguageServer\Protocol\{Diagnostic, DiagnosticSeverity, Range, Position};
use phpDocumentor\Reflection\DocBlockFactory;
use Microsoft\PhpParser;
use Microsoft\PhpParser\Node;
use Microsoft\PhpParser\Token;
class TreeAnalyzer
{
/** @var PhpParser\Parser */
private $parser;
/** @var DocBlockFactory */
private $docBlockFactory;
/** @var DefinitionResolver */
private $definitionResolver;
/** @var Node\SourceFileNode */
private $sourceFileNode;
/** @var Diagnostic[] */
private $diagnostics;
/** @var string */
private $content;
/** @var Node[] */
private $referenceNodes;
/** @var Definition[] */
private $definitions;
/** @var Node[] */
private $definitionNodes;
/**
* @param PhpParser\Parser $parser
* @param string $content
* @param DocBlockFactory $docBlockFactory
* @param DefinitionResolver $definitionResolver
* @param string $uri
*/
public function __construct(PhpParser\Parser $parser, string $content, DocBlockFactory $docBlockFactory, DefinitionResolver $definitionResolver, string $uri)
{
$this->parser = $parser;
$this->docBlockFactory = $docBlockFactory;
$this->definitionResolver = $definitionResolver;
$this->sourceFileNode = $this->parser->parseSourceFile($content, $uri);
// TODO - docblock errors
$this->traverse($this->sourceFileNode);
}
/**
* Collects Parser diagnostic messages for the Node/Token
* and transforms them into LSP Format
*
* @param Node|Token $node
* @return void
*/
private function collectDiagnostics($node)
{
// Get errors from the parser.
if (($error = PhpParser\DiagnosticsProvider::checkDiagnostics($node)) !== null) {
$range = PhpParser\PositionUtilities::getRangeFromPosition($error->start, $error->length, $this->sourceFileNode->fileContents);
switch ($error->kind) {
case PhpParser\DiagnosticKind::Error:
$severity = DiagnosticSeverity::ERROR;
break;
case PhpParser\DiagnosticKind::Warning:
default:
$severity = DiagnosticSeverity::WARNING;
break;
}
$this->diagnostics[] = new Diagnostic(
$error->message,
new Range(
new Position($range->start->line, $range->start->character),
new Position($range->end->line, $range->start->character)
),
null,
$severity,
'php'
);
}
// Check for invalid usage of $this.
if ($node instanceof Node\Expression\Variable && $node->getName() === 'this') {
// Find the first ancestor that's a class method. Return an error
// if there is none, or if the method is static.
$method = $node->getFirstAncestor(Node\MethodDeclaration::class);
if ($method && $method->isStatic()) {
$this->diagnostics[] = new Diagnostic(
"\$this can not be used in static methods.",
Range::fromNode($node),
null,
DiagnosticSeverity::ERROR,
'php'
);
}
}
}
/**
* Recursive AST traversal to collect definitions/references and diagnostics
*
* @param Node|Token $currentNode The node/token to process
*/
private function traverse($currentNode)
{
$this->collectDiagnostics($currentNode);
// Only update/descend into Nodes, Tokens are leaves
if ($currentNode instanceof Node) {
$this->collectDefinitionsAndReferences($currentNode);
foreach ($currentNode::CHILD_NAMES as $name) {
$child = $currentNode->$name;
if ($child === null) {
continue;
}
if (\is_array($child)) {
foreach ($child as $actualChild) {
if ($actualChild !== null) {
$this->traverse($actualChild);
}
}
} else {
$this->traverse($child);
}
}
}
}
/**
* Collect definitions and references for the given node
*
* @param Node $node
*/
private function collectDefinitionsAndReferences(Node $node)
{
$fqn = ($this->definitionResolver)::getDefinedFqn($node);
// Only index definitions with an FQN (no variables)
if ($fqn !== null) {
$this->definitionNodes[$fqn] = $node;
$this->definitions[$fqn] = $this->definitionResolver->createDefinitionFromNode($node, $fqn);
} else {
$parent = $node->parent;
if (
(
// $node->parent instanceof Node\Expression\ScopedPropertyAccessExpression ||
($node instanceof Node\Expression\ScopedPropertyAccessExpression ||
$node instanceof Node\Expression\MemberAccessExpression)
&& !(
$node->parent instanceof Node\Expression\CallExpression ||
$node->memberName instanceof PhpParser\Token
))
|| ($parent instanceof Node\Statement\NamespaceDefinition && $parent->name !== null && $parent->name->getStart() === $node->getStart())
) {
return;
}
$fqn = $this->definitionResolver->resolveReferenceNodeToFqn($node);
if (!$fqn) {
return;
}
if ($fqn === 'self' || $fqn === 'static') {
// Resolve self and static keywords to the containing class
// (This is not 100% correct for static but better than nothing)
$classNode = $node->getFirstAncestor(Node\Statement\ClassDeclaration::class);
if (!$classNode) {
return;
}
$fqn = (string)$classNode->getNamespacedName();
if (!$fqn) {
return;
}
} else if ($fqn === 'parent') {
// Resolve parent keyword to the base class FQN
$classNode = $node->getFirstAncestor(Node\Statement\ClassDeclaration::class);
if (!$classNode || !$classNode->classBaseClause || !$classNode->classBaseClause->baseClass) {
return;
}
$fqn = (string)$classNode->classBaseClause->baseClass->getResolvedName();
if (!$fqn) {
return;
}
}
$this->addReference($fqn, $node);
if (
$node instanceof Node\QualifiedName
&& ($node->isQualifiedName() || $node->parent instanceof Node\NamespaceUseClause)
&& !($parent instanceof Node\Statement\NamespaceDefinition && $parent->name->getStart() === $node->getStart()
)
) {
// Add references for each referenced namespace
$ns = $fqn;
while (($pos = strrpos($ns, '\\')) !== false) {
$ns = substr($ns, 0, $pos);
$this->addReference($ns, $node);
}
}
// Namespaced constant access and function calls also need to register a reference
// to the global version because PHP falls back to global at runtime
// http://php.net/manual/en/language.namespaces.fallback.php
if (ParserHelpers\isConstantFetch($node) ||
($parent instanceof Node\Expression\CallExpression
&& !(
$node instanceof Node\Expression\ScopedPropertyAccessExpression ||
$node instanceof Node\Expression\MemberAccessExpression
))) {
$parts = explode('\\', $fqn);
if (count($parts) > 1) {
$globalFqn = end($parts);
$this->addReference($globalFqn, $node);
}
}
}
}
/**
* @return Diagnostic[]
*/
public function getDiagnostics(): array
{
return $this->diagnostics ?? [];
}
/**
* @return void
*/
private function addReference(string $fqn, Node $node)
{
if (!isset($this->referenceNodes[$fqn])) {
$this->referenceNodes[$fqn] = [];
}
$this->referenceNodes[$fqn][] = $node;
}
/**
* @return Definition[]
*/
public function getDefinitions()
{
return $this->definitions ?? [];
}
/**
* @return Node[]
*/
public function getDefinitionNodes()
{
return $this->definitionNodes ?? [];
}
/**
* @return Node[]
*/
public function getReferenceNodes()
{
return $this->referenceNodes ?? [];
}
/**
* @return Node\SourceFileNode
*/
public function getSourceFileNode()
{
return $this->sourceFileNode;
}
}

View File

@ -5,7 +5,6 @@ namespace LanguageServer;
use Throwable; use Throwable;
use InvalidArgumentException; use InvalidArgumentException;
use PhpParser\Node;
use Sabre\Event\{Loop, Promise, EmitterInterface}; use Sabre\Event\{Loop, Promise, EmitterInterface};
use Sabre\Uri; use Sabre\Uri;
@ -94,23 +93,6 @@ function waitForEvent(EmitterInterface $emitter, string $event): Promise
return $p; return $p;
} }
/**
* Returns the closest node of a specific type
*
* @param Node $node
* @param string $type The node class name
* @return Node|null $type
*/
function getClosestNode(Node $node, string $type)
{
$n = $node;
while ($n = $n->getAttribute('parentNode')) {
if ($n instanceof $type) {
return $n;
}
}
}
/** /**
* Returns the part of $b that is not overlapped by $a * Returns the part of $b that is not overlapped by $a
* Example: * Example:
@ -166,9 +148,8 @@ function isVendored(PhpDocument $document, \stdClass $composerJson = null): bool
* Check a given URI against the composer.json to see if it * Check a given URI against the composer.json to see if it
* is a vendored URI * is a vendored URI
* *
* @param \stdClass|null $composerJson
* @param string $uri * @param string $uri
* @param array $matches * @param \stdClass|null $composerJson
* @return string|null * @return string|null
*/ */
function getPackageName(string $uri, \stdClass $composerJson = null) function getPackageName(string $uri, \stdClass $composerJson = null)

View File

@ -0,0 +1,65 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Tests;
use PHPUnit\Framework\TestCase;
use LanguageServer\Index\Index;
use LanguageServer\DefinitionResolver;
use Microsoft\PhpParser;
class DefinitionResolverTest extends TestCase
{
public function testCreateDefinitionFromNode()
{
$parser = new PhpParser\Parser;
$doc = new MockPhpDocument;
$sourceFileNode = $parser->parseSourceFile("<?php\ndefine('TEST_DEFINE', true);", $doc->getUri());
$index = new Index;
$definitionResolver = new DefinitionResolver($index);
$def = $definitionResolver->createDefinitionFromNode($sourceFileNode->statementList[1]->expression, '\TEST_DEFINE');
$this->assertInstanceOf(\phpDocumentor\Reflection\Types\Boolean::class, $def->type);
}
public function testGetTypeFromNode()
{
$parser = new PhpParser\Parser;
$doc = new MockPhpDocument;
$sourceFileNode = $parser->parseSourceFile("<?php\ndefine('TEST_DEFINE', true);", $doc->getUri());
$index = new Index;
$definitionResolver = new DefinitionResolver($index);
$type = $definitionResolver->getTypeFromNode($sourceFileNode->statementList[1]->expression);
$this->assertInstanceOf(\phpDocumentor\Reflection\Types\Boolean::class, $type);
}
public function testGetDefinedFqnForIncompleteDefine()
{
// define('XXX') (only one argument) must not introduce a new symbol
$parser = new PhpParser\Parser;
$doc = new MockPhpDocument;
$sourceFileNode = $parser->parseSourceFile("<?php\ndefine('TEST_DEFINE');", $doc->getUri());
$index = new Index;
$definitionResolver = new DefinitionResolver($index);
$fqn = $definitionResolver->getDefinedFqn($sourceFileNode->statementList[1]->expression);
$this->assertNull($fqn);
}
public function testGetDefinedFqnForDefine()
{
$parser = new PhpParser\Parser;
$doc = new MockPhpDocument;
$sourceFileNode = $parser->parseSourceFile("<?php\ndefine('TEST_DEFINE', true);", $doc->getUri());
$index = new Index;
$definitionResolver = new DefinitionResolver($index);
$fqn = $definitionResolver->getDefinedFqn($sourceFileNode->statementList[1]->expression);
$this->assertEquals('TEST_DEFINE', $fqn);
}
}

View File

@ -0,0 +1,82 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Tests\Diagnostics;
use PHPUnit\Framework\TestCase;
use phpDocumentor\Reflection\DocBlockFactory;
use LanguageServer\{
DefinitionResolver, TreeAnalyzer
};
use LanguageServer\Index\{Index};
use LanguageServer\Protocol\{
Diagnostic, DiagnosticSeverity, Position, Range
};
use function LanguageServer\pathToUri;
use Microsoft\PhpParser\Parser;
class InvalidThisUsageTest extends TestCase
{
/**
* Parse the given file and return diagnostics.
*
* @param string $path
* @return Diagnostic[]
*/
private function collectDiagnostics(string $path): array
{
$uri = pathToUri($path);
$parser = new Parser();
$docBlockFactory = DocBlockFactory::createInstance();
$index = new Index;
$definitionResolver = new DefinitionResolver($index);
$content = file_get_contents($path);
$treeAnalyzer = new TreeAnalyzer($parser, $content, $docBlockFactory, $definitionResolver, $uri);
return $treeAnalyzer->getDiagnostics();
}
/**
* Assertions about a diagnostic.
*
* @param Diagnostic|null $diagnostic
* @param int $message
* @param string $severity
* @param Range $range
*/
private function assertDiagnostic($diagnostic, $message, $severity, $range)
{
$this->assertInstanceOf(Diagnostic::class, $diagnostic);
$this->assertEquals($message, $diagnostic->message);
$this->assertEquals($severity, $diagnostic->severity);
$this->assertEquals($range, $diagnostic->range);
}
public function testThisInStaticMethodProducesError()
{
$diagnostics = $this->collectDiagnostics(
__DIR__ . '/../../fixtures/diagnostics/errors/this_in_static_method.php'
);
$this->assertCount(1, $diagnostics);
$this->assertDiagnostic(
$diagnostics[0],
'$this can not be used in static methods.',
DiagnosticSeverity::ERROR,
new Range(
new Position(6, 15),
new Position(6, 20)
)
);
}
public function testThisInMethodProducesNoError()
{
$diagnostics = $this->collectDiagnostics(
__DIR__ . '/../../fixtures/diagnostics/baselines/this_in_method.php'
);
$this->assertCount(0, $diagnostics);
}
}

View File

@ -1,28 +0,0 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Tests;
use PHPUnit\Framework\TestCase;
use LanguageServer\Formatter;
class FormatterTest extends TestCase
{
public function testFormat()
{
$input = file_get_contents(__DIR__ . '/../fixtures/format.php');
$output = file_get_contents(__DIR__ . '/../fixtures/format_expected.php');
$edits = Formatter::format($input, 'file:///whatever');
$this->assertSame($output, $edits[0]->newText);
}
public function testFormatNoChange()
{
$expected = file_get_contents(__DIR__ . '/../fixtures/format_expected.php');
$edits = Formatter::format($expected, 'file:///whatever');
$this->assertSame([], $edits);
}
}

30
tests/Index/IndexTest.php Normal file
View File

@ -0,0 +1,30 @@
<?php
namespace LanguageServer\Tests;
use PHPUnit\Framework\TestCase;
use LanguageServer\Index\Index;
use LanguageServer\Definition;
class IndexTest extends TestCase
{
public function testGetSetMethodDefinition()
{
$index = new Index;
$index->setDefinition('SomeNamespace\SomeClass', new Definition);
$methodDefinition = new Definition;
$methodFqn = 'SomeNamespace\SomeClass->someMethod()';
$index->setDefinition($methodFqn, $methodDefinition);
$index->setDefinition('SomeNamespace\SomeClass->someProperty', new Definition);
$this->assertSame($methodDefinition, $index->getDefinition($methodFqn));
}
public function testGetSetClassDefinition()
{
$index = new Index;
$definition = new Definition;
$fqn = 'SomeNamespace\SomeClass';
$index->setDefinition($fqn, $definition);
$this->assertSame($definition, $index->getDefinition($fqn));
}
}

View File

@ -15,7 +15,8 @@ use LanguageServer\Protocol\{
TextDocumentIdentifier, TextDocumentIdentifier,
InitializeResult, InitializeResult,
ServerCapabilities, ServerCapabilities,
CompletionOptions CompletionOptions,
SignatureHelpOptions
}; };
use AdvancedJsonRpc; use AdvancedJsonRpc;
use Webmozart\Glob\Glob; use Webmozart\Glob\Glob;
@ -35,13 +36,14 @@ class LanguageServerTest extends TestCase
$serverCapabilities->textDocumentSync = TextDocumentSyncKind::FULL; $serverCapabilities->textDocumentSync = TextDocumentSyncKind::FULL;
$serverCapabilities->documentSymbolProvider = true; $serverCapabilities->documentSymbolProvider = true;
$serverCapabilities->workspaceSymbolProvider = true; $serverCapabilities->workspaceSymbolProvider = true;
$serverCapabilities->documentFormattingProvider = true;
$serverCapabilities->definitionProvider = true; $serverCapabilities->definitionProvider = true;
$serverCapabilities->referencesProvider = true; $serverCapabilities->referencesProvider = true;
$serverCapabilities->hoverProvider = true; $serverCapabilities->hoverProvider = true;
$serverCapabilities->completionProvider = new CompletionOptions; $serverCapabilities->completionProvider = new CompletionOptions;
$serverCapabilities->completionProvider->resolveProvider = false; $serverCapabilities->completionProvider->resolveProvider = false;
$serverCapabilities->completionProvider->triggerCharacters = ['$', '>']; $serverCapabilities->completionProvider->triggerCharacters = ['$', '>'];
$serverCapabilities->signatureHelpProvider = new SignatureHelpOptions;
$serverCapabilities->signatureHelpProvider->triggerCharacters = ['(', ','];
$serverCapabilities->xworkspaceReferencesProvider = true; $serverCapabilities->xworkspaceReferencesProvider = true;
$serverCapabilities->xdefinitionProvider = true; $serverCapabilities->xdefinitionProvider = true;
$serverCapabilities->xdependenciesProvider = true; $serverCapabilities->xdependenciesProvider = true;
@ -58,7 +60,7 @@ class LanguageServerTest extends TestCase
if ($msg->body->method === 'window/logMessage' && $promise->state === Promise::PENDING) { if ($msg->body->method === 'window/logMessage' && $promise->state === Promise::PENDING) {
if ($msg->body->params->type === MessageType::ERROR) { if ($msg->body->params->type === MessageType::ERROR) {
$promise->reject(new Exception($msg->body->params->message)); $promise->reject(new Exception($msg->body->params->message));
} else if (strpos($msg->body->params->message, 'All 26 PHP files parsed') !== false) { } else if (preg_match('/All \d+ PHP files parsed/', $msg->body->params->message)) {
$promise->fulfill(); $promise->fulfill();
} }
} }
@ -104,7 +106,7 @@ class LanguageServerTest extends TestCase
if ($promise->state === Promise::PENDING) { if ($promise->state === Promise::PENDING) {
$promise->reject(new Exception($msg->body->params->message)); $promise->reject(new Exception($msg->body->params->message));
} }
} else if (strpos($msg->body->params->message, 'All 26 PHP files parsed') !== false) { } else if (preg_match('/All \d+ PHP files parsed/', $msg->body->params->message)) {
$promise->fulfill(); $promise->fulfill();
} }
} }

20
tests/MockPhpDocument.php Normal file
View File

@ -0,0 +1,20 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Tests;
/**
* A fake document for tests
*/
class MockPhpDocument
{
/**
* Returns fake uri
*
* @return string
*/
public function getUri()
{
return 'file:///whatever';
}
}

View File

@ -4,39 +4,21 @@ declare(strict_types = 1);
namespace LanguageServer\Tests\Server\TextDocument; namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use PhpParser\{NodeTraverser, Node};
use PhpParser\NodeVisitor\NameResolver;
use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\DocBlockFactory;
use LanguageServer\{LanguageClient, PhpDocument, PhpDocumentLoader, Parser, DefinitionResolver}; use LanguageServer\{
use LanguageServer\ContentRetriever\FileSystemContentRetriever; DefinitionResolver, TreeAnalyzer
use LanguageServer\Protocol\ClientCapabilities; };
use LanguageServer\Index\{ProjectIndex, Index, DependenciesIndex}; use LanguageServer\Index\{Index};
use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\NodeVisitor\{ReferencesAdder, DefinitionCollector};
use function LanguageServer\pathToUri; use function LanguageServer\pathToUri;
use Microsoft\PhpParser;
use Microsoft\PhpParser\Node;
class DefinitionCollectorTest extends TestCase class DefinitionCollectorTest extends TestCase
{ {
public function testCollectsSymbols() public function testCollectsSymbols()
{ {
$path = realpath(__DIR__ . '/../../fixtures/symbols.php'); $path = realpath(__DIR__ . '/../../fixtures/symbols.php');
$uri = pathToUri($path); $defNodes = $this->collectDefinitions($path);
$parser = new Parser;
$docBlockFactory = DocBlockFactory::createInstance();
$index = new Index;
$definitionResolver = new DefinitionResolver($index);
$content = file_get_contents($path);
$document = new PhpDocument($uri, $content, $index, $parser, $docBlockFactory, $definitionResolver);
$stmts = $parser->parse($content);
$traverser = new NodeTraverser;
$traverser->addVisitor(new NameResolver);
$traverser->addVisitor(new ReferencesAdder($document));
$definitionCollector = new DefinitionCollector($definitionResolver);
$traverser->addVisitor($definitionCollector);
$traverser->traverse($stmts);
$defNodes = $definitionCollector->nodes;
$this->assertEquals([ $this->assertEquals([
'TestNamespace', 'TestNamespace',
@ -50,45 +32,53 @@ class DefinitionCollectorTest extends TestCase
'TestNamespace\\TestTrait', 'TestNamespace\\TestTrait',
'TestNamespace\\TestInterface', 'TestNamespace\\TestInterface',
'TestNamespace\\test_function()', 'TestNamespace\\test_function()',
'TestNamespace\\ChildClass' 'TestNamespace\\ChildClass',
'TestNamespace\\Example',
'TestNamespace\\Example->__construct()',
'TestNamespace\\Example->__destruct()'
], array_keys($defNodes)); ], array_keys($defNodes));
$this->assertInstanceOf(Node\Const_::class, $defNodes['TestNamespace\\TEST_CONST']);
$this->assertInstanceOf(Node\Stmt\Class_::class, $defNodes['TestNamespace\\TestClass']); $this->assertInstanceOf(Node\ConstElement::class, $defNodes['TestNamespace\\TEST_CONST']);
$this->assertInstanceOf(Node\Const_::class, $defNodes['TestNamespace\\TestClass::TEST_CLASS_CONST']); $this->assertInstanceOf(Node\Statement\ClassDeclaration::class, $defNodes['TestNamespace\\TestClass']);
$this->assertInstanceOf(Node\Stmt\PropertyProperty::class, $defNodes['TestNamespace\\TestClass::$staticTestProperty']); $this->assertInstanceOf(Node\ConstElement::class, $defNodes['TestNamespace\\TestClass::TEST_CLASS_CONST']);
$this->assertInstanceOf(Node\Stmt\PropertyProperty::class, $defNodes['TestNamespace\\TestClass->testProperty']); // TODO - should we parse properties more strictly?
$this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defNodes['TestNamespace\\TestClass::staticTestMethod()']); $this->assertInstanceOf(Node\Expression\Variable::class, $defNodes['TestNamespace\\TestClass::$staticTestProperty']);
$this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defNodes['TestNamespace\\TestClass->testMethod()']); $this->assertInstanceOf(Node\Expression\Variable::class, $defNodes['TestNamespace\\TestClass->testProperty']);
$this->assertInstanceOf(Node\Stmt\Trait_::class, $defNodes['TestNamespace\\TestTrait']); $this->assertInstanceOf(Node\MethodDeclaration::class, $defNodes['TestNamespace\\TestClass::staticTestMethod()']);
$this->assertInstanceOf(Node\Stmt\Interface_::class, $defNodes['TestNamespace\\TestInterface']); $this->assertInstanceOf(Node\MethodDeclaration::class, $defNodes['TestNamespace\\TestClass->testMethod()']);
$this->assertInstanceOf(Node\Stmt\Function_::class, $defNodes['TestNamespace\\test_function()']); $this->assertInstanceOf(Node\Statement\TraitDeclaration::class, $defNodes['TestNamespace\\TestTrait']);
$this->assertInstanceOf(Node\Stmt\Class_::class, $defNodes['TestNamespace\\ChildClass']); $this->assertInstanceOf(Node\Statement\InterfaceDeclaration::class, $defNodes['TestNamespace\\TestInterface']);
$this->assertInstanceOf(Node\Statement\FunctionDeclaration::class, $defNodes['TestNamespace\\test_function()']);
$this->assertInstanceOf(Node\Statement\ClassDeclaration::class, $defNodes['TestNamespace\\ChildClass']);
$this->assertInstanceOf(Node\Statement\ClassDeclaration::class, $defNodes['TestNamespace\\Example']);
$this->assertInstanceOf(Node\MethodDeclaration::class, $defNodes['TestNamespace\\Example->__construct()']);
$this->assertInstanceOf(Node\MethodDeclaration::class, $defNodes['TestNamespace\\Example->__destruct()']);
} }
public function testDoesNotCollectReferences() public function testDoesNotCollectReferences()
{ {
$path = realpath(__DIR__ . '/../../fixtures/references.php'); $path = realpath(__DIR__ . '/../../fixtures/references.php');
$defNodes = $this->collectDefinitions($path);
$this->assertEquals(['TestNamespace', 'TestNamespace\\whatever()'], array_keys($defNodes));
$this->assertInstanceOf(Node\Statement\NamespaceDefinition::class, $defNodes['TestNamespace']);
$this->assertInstanceOf(Node\Statement\FunctionDeclaration::class, $defNodes['TestNamespace\\whatever()']);
}
/**
* @param $path
*/
private function collectDefinitions(string $path): array
{
$uri = pathToUri($path); $uri = pathToUri($path);
$parser = new Parser; $parser = new PhpParser\Parser();
$docBlockFactory = DocBlockFactory::createInstance(); $docBlockFactory = DocBlockFactory::createInstance();
$index = new Index; $index = new Index;
$definitionResolver = new DefinitionResolver($index); $definitionResolver = new DefinitionResolver($index);
$content = file_get_contents($path); $content = file_get_contents($path);
$document = new PhpDocument($uri, $content, $index, $parser, $docBlockFactory, $definitionResolver);
$stmts = $parser->parse($content);
$traverser = new NodeTraverser; $treeAnalyzer = new TreeAnalyzer($parser, $content, $docBlockFactory, $definitionResolver, $uri);
$traverser->addVisitor(new NameResolver); return $treeAnalyzer->getDefinitionNodes();
$traverser->addVisitor(new ReferencesAdder($document));
$definitionCollector = new DefinitionCollector($definitionResolver);
$traverser->addVisitor($definitionCollector);
$traverser->traverse($stmts);
$defNodes = $definitionCollector->nodes;
$this->assertEquals(['TestNamespace', 'TestNamespace\\whatever()'], array_keys($defNodes));
$this->assertInstanceOf(Node\Name::class, $defNodes['TestNamespace']);
$this->assertInstanceOf(Node\Stmt\Namespace_::class, $defNodes['TestNamespace']->getAttribute('parentNode'));
$this->assertInstanceOf(Node\Stmt\Function_::class, $defNodes['TestNamespace\\whatever()']);
} }
} }

View File

@ -3,20 +3,14 @@ declare(strict_types = 1);
namespace LanguageServer\Tests\Server; namespace LanguageServer\Tests\Server;
use PHPUnit\Framework\TestCase; use LanguageServer\{
use LanguageServer\Tests\MockProtocolStream; PhpDocument, PhpDocumentLoader, Project, DefinitionResolver
use LanguageServer\{Server, Client, LanguageClient, Project, PhpDocument, PhpDocumentLoader, DefinitionResolver};
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\Protocol\{
TextDocumentItem,
TextDocumentIdentifier,
SymbolKind,
DiagnosticSeverity,
FormattingOptions,
ClientCapabilities
}; };
use AdvancedJsonRpc\{Request as RequestBody, Response as ResponseBody}; use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Index\{
DependenciesIndex, Index, ProjectIndex
};
use PHPUnit\Framework\TestCase;
use function LanguageServer\pathToUri; use function LanguageServer\pathToUri;
class PhpDocumentLoaderTest extends TestCase class PhpDocumentLoaderTest extends TestCase

View File

@ -3,22 +3,26 @@ declare(strict_types = 1);
namespace LanguageServer\Tests\Server; namespace LanguageServer\Tests\Server;
use PHPUnit\Framework\TestCase; use LanguageServer\{
PhpDocument, DefinitionResolver
};
use LanguageServer\Index\{
Index
};
use LanguageServer\Protocol\{
Position
};
use Microsoft\PhpParser;
use Microsoft\PhpParser\Node;
use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\DocBlockFactory;
use LanguageServer\Tests\MockProtocolStream; use PHPUnit\Framework\TestCase;
use LanguageServer\{LanguageClient, PhpDocument, DefinitionResolver, Parser};
use LanguageServer\NodeVisitor\NodeAtPositionFinder;
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\{SymbolKind, Position, ClientCapabilities};
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use PhpParser\Node;
use function LanguageServer\isVendored; use function LanguageServer\isVendored;
class PhpDocumentTest extends TestCase class PhpDocumentTest extends TestCase
{ {
public function createDocument(string $uri, string $content) public function createDocument(string $uri, string $content)
{ {
$parser = new Parser; $parser = new PhpParser\Parser();
$docBlockFactory = DocBlockFactory::createInstance(); $docBlockFactory = DocBlockFactory::createInstance();
$index = new Index; $index = new Index;
$definitionResolver = new DefinitionResolver($index); $definitionResolver = new DefinitionResolver($index);
@ -36,10 +40,15 @@ class PhpDocumentTest extends TestCase
{ {
$document = $this->createDocument('whatever', "<?php\n$\$a = new SomeClass;"); $document = $this->createDocument('whatever', "<?php\n$\$a = new SomeClass;");
$node = $document->getNodeAtPosition(new Position(1, 13)); $node = $document->getNodeAtPosition(new Position(1, 13));
$this->assertInstanceOf(Node\Name\FullyQualified::class, $node); $this->assertQualifiedName($node);
$this->assertEquals('SomeClass', (string)$node); $this->assertEquals('SomeClass', (string)$node);
} }
private function assertQualifiedName($node)
{
$this->assertInstanceOf(Node\QualifiedName::class, $node);
}
public function testIsVendored() public function testIsVendored()
{ {
$document = $this->createDocument('file:///dir/vendor/x.php', "<?php\n$\$a = new SomeClass;"); $document = $this->createDocument('file:///dir/vendor/x.php', "<?php\n$\$a = new SomeClass;");

View File

@ -5,14 +5,13 @@ namespace LanguageServer\Tests\Server;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, LanguageClient, PhpDocumentLoader, DefinitionResolver, Options, Indexer}; use LanguageServer\{
use LanguageServer\Index\{ProjectIndex, StubsIndex, GlobalIndex, DependenciesIndex, Index}; Server, LanguageClient, PhpDocumentLoader, DefinitionResolver
};
use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index};
use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\{Position, Location, Range, ClientCapabilities}; use LanguageServer\Protocol\{Position, Location, Range};
use LanguageServer\FilesFinder\FileSystemFilesFinder;
use LanguageServer\Cache\FileSystemCache;
use function LanguageServer\pathToUri; use function LanguageServer\pathToUri;
use Sabre\Event\Promise;
abstract class ServerTestCase extends TestCase abstract class ServerTestCase extends TestCase
{ {
@ -47,22 +46,16 @@ abstract class ServerTestCase extends TestCase
public function setUp() public function setUp()
{ {
$sourceIndex = new Index; $sourceIndex = new Index;
$dependenciesIndex = new DependenciesIndex; $dependenciesIndex = new DependenciesIndex;
$projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex); $projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex);
$projectIndex->setComplete(); $projectIndex->setComplete();
$rootPath = realpath(__DIR__ . '/../../fixtures/');
$options = new Options;
$filesFinder = new FileSystemFilesFinder;
$cache = new FileSystemCache;
$definitionResolver = new DefinitionResolver($projectIndex); $definitionResolver = new DefinitionResolver($projectIndex);
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream); $client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
$this->documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver); $this->documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver);
$this->textDocument = new Server\TextDocument($this->documentLoader, $definitionResolver, $client, $projectIndex); $this->textDocument = new Server\TextDocument($this->documentLoader, $definitionResolver, $client, $projectIndex);
$indexer = new Indexer($filesFinder, $rootPath, $client, $cache, $dependenciesIndex, $sourceIndex, $this->documentLoader, null, null, $options); $this->workspace = new Server\Workspace($client, $projectIndex, $dependenciesIndex, $sourceIndex, null, $this->documentLoader);
$this->workspace = new Server\Workspace($client, $projectIndex, $dependenciesIndex, $sourceIndex, null, $this->documentLoader, null, $indexer, $options);
$globalSymbolsUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_symbols.php')); $globalSymbolsUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_symbols.php'));
$globalReferencesUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_references.php')); $globalReferencesUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_references.php'));
@ -80,34 +73,41 @@ abstract class ServerTestCase extends TestCase
$this->definitionLocations = [ $this->definitionLocations = [
// Global // Global
'TEST_CONST' => new Location($globalSymbolsUri, new Range(new Position( 9, 6), new Position( 9, 22))), 'TEST_DEFINE_CONSTANT' => new Location($globalSymbolsUri, new Range(new Position(104, 0), new Position(104, 37))),
'TestClass' => new Location($globalSymbolsUri, new Range(new Position(20, 0), new Position(61, 1))), 'TEST_CONST' => new Location($globalSymbolsUri, new Range(new Position( 9, 6), new Position( 9, 22))),
'ChildClass' => new Location($globalSymbolsUri, new Range(new Position(99, 0), new Position(99, 37))), 'TestClass' => new Location($globalSymbolsUri, new Range(new Position(20, 0), new Position(61, 1))),
'TestTrait' => new Location($globalSymbolsUri, new Range(new Position(63, 0), new Position(66, 1))), 'ChildClass' => new Location($globalSymbolsUri, new Range(new Position(99, 0), new Position(99, 37))),
'TestInterface' => new Location($globalSymbolsUri, new Range(new Position(68, 0), new Position(71, 1))), 'TestTrait' => new Location($globalSymbolsUri, new Range(new Position(63, 0), new Position(66, 1))),
'TestClass::TEST_CLASS_CONST' => new Location($globalSymbolsUri, new Range(new Position(27, 10), new Position(27, 32))), 'TestInterface' => new Location($globalSymbolsUri, new Range(new Position(68, 0), new Position(71, 1))),
'TestClass::testProperty' => new Location($globalSymbolsUri, new Range(new Position(41, 11), new Position(41, 24))), 'TestClass::TEST_CLASS_CONST' => new Location($globalSymbolsUri, new Range(new Position(27, 10), new Position(27, 32))),
'TestClass::staticTestProperty' => new Location($globalSymbolsUri, new Range(new Position(34, 18), new Position(34, 37))), 'TestClass::testProperty' => new Location($globalSymbolsUri, new Range(new Position(41, 11), new Position(41, 24))),
'TestClass::staticTestMethod()' => new Location($globalSymbolsUri, new Range(new Position(46, 4), new Position(49, 5))), 'TestClass::staticTestProperty' => new Location($globalSymbolsUri, new Range(new Position(34, 18), new Position(34, 37))),
'TestClass::testMethod()' => new Location($globalSymbolsUri, new Range(new Position(57, 4), new Position(60, 5))), 'TestClass::staticTestMethod()' => new Location($globalSymbolsUri, new Range(new Position(46, 4), new Position(49, 5))),
'test_function()' => new Location($globalSymbolsUri, new Range(new Position(78, 0), new Position(81, 1))), 'TestClass::testMethod()' => new Location($globalSymbolsUri, new Range(new Position(57, 4), new Position(60, 5))),
'whatever()' => new Location($globalReferencesUri, new Range(new Position(21, 0), new Position(23, 1))), 'test_function()' => new Location($globalSymbolsUri, new Range(new Position(78, 0), new Position(81, 1))),
'UnusedClass' => new Location($globalSymbolsUri, new Range(new Position(111, 0), new Position(118, 1))),
'UnusedClass::unusedProperty' => new Location($globalSymbolsUri, new Range(new Position(113,11), new Position(113, 26))),
'UnusedClass::unusedMethod' => new Location($globalSymbolsUri, new Range(new Position(115, 4), new Position(117, 5))),
'whatever()' => new Location($globalReferencesUri, new Range(new Position(21, 0), new Position(23, 1))),
// Namespaced // Namespaced
'TestNamespace' => new Location($symbolsUri, new Range(new Position( 2, 10), new Position( 2, 23))), 'TestNamespace' => new Location($symbolsUri, new Range(new Position( 2, 0), new Position( 2, 24))),
'SecondTestNamespace' => new Location($useUri, new Range(new Position( 2, 10), new Position( 2, 29))), 'SecondTestNamespace' => new Location($useUri, new Range(new Position( 2, 0), new Position( 2, 30))),
'TestNamespace\\TEST_CONST' => new Location($symbolsUri, new Range(new Position( 9, 6), new Position( 9, 22))), 'TestNamespace\\TEST_CONST' => new Location($symbolsUri, new Range(new Position( 9, 6), new Position( 9, 22))),
'TestNamespace\\TestClass' => new Location($symbolsUri, new Range(new Position(20, 0), new Position(61, 1))), 'TestNamespace\\TestClass' => new Location($symbolsUri, new Range(new Position(20, 0), new Position(61, 1))),
'TestNamespace\\ChildClass' => new Location($symbolsUri, new Range(new Position(99, 0), new Position(99, 37))), 'TestNamespace\\ChildClass' => new Location($symbolsUri, new Range(new Position(99, 0), new Position(99, 37))),
'TestNamespace\\TestTrait' => new Location($symbolsUri, new Range(new Position(63, 0), new Position(66, 1))), 'TestNamespace\\TestTrait' => new Location($symbolsUri, new Range(new Position(63, 0), new Position(66, 1))),
'TestNamespace\\TestInterface' => new Location($symbolsUri, new Range(new Position(68, 0), new Position(71, 1))), 'TestNamespace\\TestInterface' => new Location($symbolsUri, new Range(new Position(68, 0), new Position(71, 1))),
'TestNamespace\\TestClass::TEST_CLASS_CONST' => new Location($symbolsUri, new Range(new Position(27, 10), new Position(27, 32))), 'TestNamespace\\TestClass::TEST_CLASS_CONST' => new Location($symbolsUri, new Range(new Position(27, 10), new Position(27, 32))),
'TestNamespace\\TestClass::testProperty' => new Location($symbolsUri, new Range(new Position(41, 11), new Position(41, 24))), 'TestNamespace\\TestClass::testProperty' => new Location($symbolsUri, new Range(new Position(41, 11), new Position(41, 24))),
'TestNamespace\\TestClass::staticTestProperty' => new Location($symbolsUri, new Range(new Position(34, 18), new Position(34, 37))), 'TestNamespace\\TestClass::staticTestProperty' => new Location($symbolsUri, new Range(new Position(34, 18), new Position(34, 37))),
'TestNamespace\\TestClass::staticTestMethod()' => new Location($symbolsUri, new Range(new Position(46, 4), new Position(49, 5))), 'TestNamespace\\TestClass::staticTestMethod()' => new Location($symbolsUri, new Range(new Position(46, 4), new Position(49, 5))),
'TestNamespace\\TestClass::testMethod()' => new Location($symbolsUri, new Range(new Position(57, 4), new Position(60, 5))), 'TestNamespace\\TestClass::testMethod()' => new Location($symbolsUri, new Range(new Position(57, 4), new Position(60, 5))),
'TestNamespace\\test_function()' => new Location($symbolsUri, new Range(new Position(78, 0), new Position(81, 1))), 'TestNamespace\\test_function()' => new Location($symbolsUri, new Range(new Position(78, 0), new Position(81, 1))),
'TestNamespace\\whatever()' => new Location($referencesUri, new Range(new Position(21, 0), new Position(23, 1))) 'TestNamespace\\whatever()' => new Location($referencesUri, new Range(new Position(21, 0), new Position(23, 1))),
'TestNamespace\\Example' => new Location($symbolsUri, new Range(new Position(101, 0), new Position(104, 1))),
'TestNamespace\\Example::__construct' => new Location($symbolsUri, new Range(new Position(102, 4), new Position(102, 36))),
'TestNamespace\\Example::__destruct' => new Location($symbolsUri, new Range(new Position(103, 4), new Position(103, 35)))
]; ];
$this->referenceLocations = [ $this->referenceLocations = [
@ -116,21 +116,22 @@ abstract class ServerTestCase extends TestCase
'TestNamespace' => [ 'TestNamespace' => [
0 => new Location($referencesUri, new Range(new Position(31, 13), new Position(31, 40))), // use function TestNamespace\test_function; 0 => new Location($referencesUri, new Range(new Position(31, 13), new Position(31, 40))), // use function TestNamespace\test_function;
1 => new Location($useUri, new Range(new Position( 4, 4), new Position( 4, 27))), // use TestNamespace\TestClass; 1 => new Location($useUri, new Range(new Position( 4, 4), new Position( 4, 27))), // use TestNamespace\TestClass;
2 => new Location($useUri, new Range(new Position( 5, 4), new Position( 5, 17))) // use TestNamespace\{TestTrait, TestInterface}; 2 => new Location($useUri, new Range(new Position( 5, 4), new Position( 5, 18))) // use TestNamespace\{TestTrait, TestInterface};
], ],
'TestNamespace\\TEST_CONST' => [ 'TestNamespace\\TEST_CONST' => [
0 => new Location($referencesUri, new Range(new Position(29, 5), new Position(29, 15))) 0 => new Location($referencesUri, new Range(new Position(29, 5), new Position(29, 15)))
], ],
'TestNamespace\\TestClass' => [ 'TestNamespace\\TestClass' => [
0 => new Location($symbolsUri , new Range(new Position(99, 25), new Position(99, 34))), // class ChildClass extends TestClass {} 0 => new Location($symbolsUri, new Range(new Position(48, 13), new Position(48, 17))), // echo self::TEST_CLASS_CONST;
1 => new Location($referencesUri, new Range(new Position( 4, 11), new Position( 4, 20))), // $obj = new TestClass(); 1 => new Location($symbolsUri , new Range(new Position(99, 25), new Position(99, 34))), // class ChildClass extends TestClass {}
2 => new Location($referencesUri, new Range(new Position( 7, 0), new Position( 7, 9))), // TestClass::staticTestMethod(); 2 => new Location($referencesUri, new Range(new Position( 4, 11), new Position( 4, 20))), // $obj = new TestClass();
3 => new Location($referencesUri, new Range(new Position( 8, 5), new Position( 8, 14))), // echo TestClass::$staticTestProperty; 3 => new Location($referencesUri, new Range(new Position( 7, 0), new Position( 7, 9))), // TestClass::staticTestMethod();
4 => new Location($referencesUri, new Range(new Position( 9, 5), new Position( 9, 14))), // TestClass::TEST_CLASS_CONST; 4 => new Location($referencesUri, new Range(new Position( 8, 5), new Position( 8, 14))), // echo TestClass::$staticTestProperty;
5 => new Location($referencesUri, new Range(new Position(21, 18), new Position(21, 27))), // function whatever(TestClass $param) 5 => new Location($referencesUri, new Range(new Position( 9, 5), new Position( 9, 14))), // TestClass::TEST_CLASS_CONST;
6 => new Location($referencesUri, new Range(new Position(21, 37), new Position(21, 46))), // function whatever(TestClass $param): TestClass 6 => new Location($referencesUri, new Range(new Position(21, 18), new Position(21, 27))), // function whatever(TestClass $param)
7 => new Location($referencesUri, new Range(new Position(39, 0), new Position(39, 9))), // TestClass::$staticTestProperty[123]->testProperty; 7 => new Location($referencesUri, new Range(new Position(21, 37), new Position(21, 46))), // function whatever(TestClass $param): TestClass
8 => new Location($useUri, new Range(new Position( 4, 4), new Position( 4, 27))), // use TestNamespace\TestClass; 8 => new Location($referencesUri, new Range(new Position(39, 0), new Position(39, 9))), // TestClass::$staticTestProperty[123]->testProperty;
9 => new Location($useUri, new Range(new Position( 4, 4), new Position( 4, 27))), // use TestNamespace\TestClass;
], ],
'TestNamespace\\TestChild' => [ 'TestNamespace\\TestChild' => [
0 => new Location($referencesUri, new Range(new Position(42, 5), new Position(42, 25))), // echo $child->testProperty; 0 => new Location($referencesUri, new Range(new Position(42, 5), new Position(42, 25))), // echo $child->testProperty;
@ -151,16 +152,16 @@ abstract class ServerTestCase extends TestCase
3 => new Location($referencesUri, new Range(new Position(39, 0), new Position(39, 49))) // TestClass::$staticTestProperty[123]->testProperty; 3 => new Location($referencesUri, new Range(new Position(39, 0), new Position(39, 49))) // TestClass::$staticTestProperty[123]->testProperty;
], ],
'TestNamespace\\TestClass::staticTestProperty' => [ 'TestNamespace\\TestClass::staticTestProperty' => [
0 => new Location($referencesUri, new Range(new Position( 8, 5), new Position( 8, 35))), // echo TestClass::$staticTestProperty; 0 => new Location($referencesUri, new Range(new Position( 8, 16), new Position( 8, 35))), // echo TestClass::$staticTestProperty;
1 => new Location($referencesUri, new Range(new Position(39, 0), new Position(39, 30))) // TestClass::$staticTestProperty[123]->testProperty; 1 => new Location($referencesUri, new Range(new Position(39, 11), new Position(39, 30))) // TestClass::$staticTestProperty[123]->testProperty;
], ],
'TestNamespace\\TestClass::staticTestMethod()' => [ 'TestNamespace\\TestClass::staticTestMethod()' => [
0 => new Location($referencesUri, new Range(new Position( 7, 0), new Position( 7, 29))) 0 => new Location($referencesUri, new Range(new Position( 7, 0), new Position( 7, 27)))
], ],
'TestNamespace\\TestClass::testMethod()' => [ 'TestNamespace\\TestClass::testMethod()' => [
0 => new Location($referencesUri, new Range(new Position( 5, 0), new Position( 5, 18))), // $obj->testMethod(); 0 => new Location($referencesUri, new Range(new Position( 5, 0), new Position( 5, 16))), // $obj->testMethod();
1 => new Location($referencesUri, new Range(new Position(38, 0), new Position(38, 32))), // $obj->testProperty->testMethod(); 1 => new Location($referencesUri, new Range(new Position(38, 0), new Position(38, 30))), // $obj->testProperty->testMethod();
2 => new Location($referencesUri, new Range(new Position(42, 5), new Position(42, 25))) // $child->testMethod(); 2 => new Location($referencesUri, new Range(new Position(42, 5), new Position(42, 23))) // $child->testMethod();
], ],
'TestNamespace\\test_function()' => [ 'TestNamespace\\test_function()' => [
0 => new Location($referencesUri, new Range(new Position(10, 0), new Position(10, 13))), 0 => new Location($referencesUri, new Range(new Position(10, 0), new Position(10, 13))),
@ -168,26 +169,30 @@ abstract class ServerTestCase extends TestCase
], ],
// Global // Global
'TEST_DEFINE_CONSTANT' => [
0 => new Location($globalSymbolsUri, new Range(new Position(106, 6), new Position(106, 26)))
],
'TEST_CONST' => [ 'TEST_CONST' => [
0 => new Location($referencesUri, new Range(new Position(29, 5), new Position(29, 15))), 0 => new Location($referencesUri, new Range(new Position(29, 5), new Position(29, 15))),
1 => new Location($globalReferencesUri, new Range(new Position(29, 5), new Position(29, 15))) 1 => new Location($globalReferencesUri, new Range(new Position(29, 5), new Position(29, 15)))
], ],
'TestClass' => [ 'TestClass' => [
0 => new Location($globalSymbolsUri, new Range(new Position(99, 25), new Position(99, 34))), // class ChildClass extends TestClass {} 0 => new Location($globalSymbolsUri, new Range(new Position(48, 13), new Position(48, 17))), // echo self::TEST_CLASS_CONST;
1 => new Location($globalReferencesUri, new Range(new Position( 4, 11), new Position( 4, 20))), // $obj = new TestClass(); 1 => new Location($globalSymbolsUri, new Range(new Position(99, 25), new Position(99, 34))), // class ChildClass extends TestClass {}
2 => new Location($globalReferencesUri, new Range(new Position( 7, 0), new Position( 7, 9))), // TestClass::staticTestMethod(); 2 => new Location($globalReferencesUri, new Range(new Position( 4, 11), new Position( 4, 20))), // $obj = new TestClass();
3 => new Location($globalReferencesUri, new Range(new Position( 8, 5), new Position( 8, 14))), // echo TestClass::$staticTestProperty; 3 => new Location($globalReferencesUri, new Range(new Position( 7, 0), new Position( 7, 9))), // TestClass::staticTestMethod();
4 => new Location($globalReferencesUri, new Range(new Position( 9, 5), new Position( 9, 14))), // TestClass::TEST_CLASS_CONST; 4 => new Location($globalReferencesUri, new Range(new Position( 8, 5), new Position( 8, 14))), // echo TestClass::$staticTestProperty;
5 => new Location($globalReferencesUri, new Range(new Position(21, 18), new Position(21, 27))), // function whatever(TestClass $param) 5 => new Location($globalReferencesUri, new Range(new Position( 9, 5), new Position( 9, 14))), // TestClass::TEST_CLASS_CONST;
6 => new Location($globalReferencesUri, new Range(new Position(21, 37), new Position(21, 46))), // function whatever(TestClass $param): TestClass 6 => new Location($globalReferencesUri, new Range(new Position(21, 18), new Position(21, 27))), // function whatever(TestClass $param)
7 => new Location($globalReferencesUri, new Range(new Position(39, 0), new Position(39, 9))), // TestClass::$staticTestProperty[123]->testProperty; 7 => new Location($globalReferencesUri, new Range(new Position(21, 37), new Position(21, 46))), // function whatever(TestClass $param): TestClass
8 => new Location($globalReferencesUri, new Range(new Position(39, 0), new Position(39, 9))), // TestClass::$staticTestProperty[123]->testProperty;
], ],
'TestChild' => [ 'TestChild' => [
0 => new Location($globalReferencesUri, new Range(new Position(42, 5), new Position(42, 25))), // echo $child->testProperty; 0 => new Location($globalReferencesUri, new Range(new Position(42, 5), new Position(42, 25))), // echo $child->testProperty;
], ],
'TestInterface' => [ 'TestInterface' => [
0 => new Location($globalSymbolsUri, new Range(new Position(20, 27), new Position(20, 40))), // class TestClass implements TestInterface 0 => new Location($globalSymbolsUri, new Range(new Position(20, 27), new Position(20, 40))), // class TestClass implements TestInterface
1 => new Location($globalSymbolsUri, new Range(new Position(57, 48), new Position(57, 61))), // public function testMethod($testParameter): TestInterface 1 => new Location($globalSymbolsUri, new Range(new Position(57, 49), new Position(57, 61))), // public function testMethod($testParameter) : TestInterface
2 => new Location($globalReferencesUri, new Range(new Position(33, 20), new Position(33, 33))) // if ($abc instanceof TestInterface) 2 => new Location($globalReferencesUri, new Range(new Position(33, 20), new Position(33, 33))) // if ($abc instanceof TestInterface)
], ],
'TestClass::TEST_CLASS_CONST' => [ 'TestClass::TEST_CLASS_CONST' => [
@ -201,16 +206,16 @@ abstract class ServerTestCase extends TestCase
3 => new Location($globalReferencesUri, new Range(new Position(39, 0), new Position(39, 49))) // TestClass::$staticTestProperty[123]->testProperty; 3 => new Location($globalReferencesUri, new Range(new Position(39, 0), new Position(39, 49))) // TestClass::$staticTestProperty[123]->testProperty;
], ],
'TestClass::staticTestProperty' => [ 'TestClass::staticTestProperty' => [
0 => new Location($globalReferencesUri, new Range(new Position( 8, 5), new Position( 8, 35))), // echo TestClass::$staticTestProperty; 0 => new Location($globalReferencesUri, new Range(new Position( 8, 16), new Position( 8, 35))), // echo TestClass::$staticTestProperty;
1 => new Location($globalReferencesUri, new Range(new Position(39, 0), new Position(39, 30))) // TestClass::$staticTestProperty[123]->testProperty; 1 => new Location($globalReferencesUri, new Range(new Position(39, 11), new Position(39, 30))) // TestClass::$staticTestProperty[123]->testProperty;
], ],
'TestClass::staticTestMethod()' => [ 'TestClass::staticTestMethod()' => [
0 => new Location($globalReferencesUri, new Range(new Position( 7, 0), new Position( 7, 29))) 0 => new Location($globalReferencesUri, new Range(new Position( 7, 0), new Position( 7, 27)))
], ],
'TestClass::testMethod()' => [ 'TestClass::testMethod()' => [
0 => new Location($globalReferencesUri, new Range(new Position( 5, 0), new Position( 5, 18))), // $obj->testMethod(); 0 => new Location($globalReferencesUri, new Range(new Position( 5, 0), new Position( 5, 16))), // $obj->testMethod();
1 => new Location($globalReferencesUri, new Range(new Position(38, 0), new Position(38, 32))), // $obj->testProperty->testMethod(); 1 => new Location($globalReferencesUri, new Range(new Position(38, 0), new Position(38, 30))), // $obj->testProperty->testMethod();
2 => new Location($globalReferencesUri, new Range(new Position(42, 5), new Position(42, 25))) // $child->testMethod(); 2 => new Location($globalReferencesUri, new Range(new Position(42, 5), new Position(42, 23))) // $child->testMethod();
], ],
'test_function()' => [ 'test_function()' => [
0 => new Location($globalReferencesUri, new Range(new Position(10, 0), new Position(10, 13))), 0 => new Location($globalReferencesUri, new Range(new Position(10, 0), new Position(10, 13))),

View File

@ -5,18 +5,21 @@ namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, LanguageClient, PhpDocumentLoader, CompletionProvider, DefinitionResolver}; use LanguageServer\{
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex, GlobalIndex, StubsIndex}; Server, LanguageClient, PhpDocumentLoader, DefinitionResolver
};
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\{ use LanguageServer\Protocol\{
TextDocumentIdentifier, TextDocumentIdentifier,
TextEdit, TextEdit,
Range, Range,
Position, Position,
ClientCapabilities,
CompletionList, CompletionList,
CompletionItem, CompletionItem,
CompletionItemKind CompletionItemKind,
CompletionContext,
CompletionTriggerKind
}; };
use function LanguageServer\pathToUri; use function LanguageServer\pathToUri;
@ -52,7 +55,7 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(3, 7) new Position(3, 7)
)->wait(); )->wait();
$this->assertEquals(new CompletionList([ $this->assertCompletionsListSubset(new CompletionList([
new CompletionItem( new CompletionItem(
'testProperty', 'testProperty',
CompletionItemKind::PROPERTY, CompletionItemKind::PROPERTY,
@ -68,6 +71,27 @@ class CompletionTest extends TestCase
], true), $items); ], true), $items);
} }
public function testGlobalFunctionInsideNamespaceAndClass()
{
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/inside_namespace_and_method.php');
$this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri),
new Position(8, 11)
)->wait();
$this->assertCompletionsListSubset(new CompletionList([
new CompletionItem(
'test_function',
CompletionItemKind::FUNCTION,
'void', // Return type
'Officia aliquip adipisicing et nulla et laboris dolore labore.',
null,
null,
'\test_function'
)
], true), $items);
}
public function testPropertyAndMethodWithoutPrefix() public function testPropertyAndMethodWithoutPrefix()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/property.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/property.php');
@ -76,7 +100,7 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(3, 6) new Position(3, 6)
)->wait(); )->wait();
$this->assertEquals(new CompletionList([ $this->assertCompletionsListSubset(new CompletionList([
new CompletionItem( new CompletionItem(
'testProperty', 'testProperty',
CompletionItemKind::PROPERTY, CompletionItemKind::PROPERTY,
@ -100,7 +124,7 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(8, 5) new Position(8, 5)
)->wait(); )->wait();
$this->assertEquals(new CompletionList([ $this->assertCompletionsListSubset(new CompletionList([
new CompletionItem( new CompletionItem(
'$var', '$var',
CompletionItemKind::VARIABLE, CompletionItemKind::VARIABLE,
@ -132,7 +156,7 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(8, 6) new Position(8, 6)
)->wait(); )->wait();
$this->assertEquals(new CompletionList([ $this->assertCompletionsListSubset(new CompletionList([
new CompletionItem( new CompletionItem(
'$param', '$param',
CompletionItemKind::VARIABLE, CompletionItemKind::VARIABLE,
@ -154,13 +178,18 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(6, 10) new Position(6, 10)
)->wait(); )->wait();
$this->assertEquals(new CompletionList([ $this->assertCompletionsListSubset(new CompletionList([
// Global TestClass definition (inserted as \TestClass) // Global TestClass definition (inserted as \TestClass)
new CompletionItem( new CompletionItem(
'TestClass', 'TestClass',
CompletionItemKind::CLASS_, CompletionItemKind::CLASS_,
null, null,
'Pariatur ut laborum tempor voluptate consequat ea deserunt.', 'Pariatur ut laborum tempor voluptate consequat ea deserunt.' . "\n\n" .
'Deserunt enim minim sunt sint ea nisi. Deserunt excepteur tempor id nostrud' . "\n" .
'laboris commodo ad commodo velit mollit qui non officia id. Nulla duis veniam' . "\n" .
'veniam officia deserunt et non dolore mollit ea quis eiusmod sit non. Occaecat' . "\n" .
'consequat sunt culpa exercitation pariatur id reprehenderit nisi incididunt Lorem' . "\n" .
'sint. Officia culpa pariatur laborum nostrud cupidatat consequat mollit.',
null, null,
null, null,
'\TestClass' '\TestClass'
@ -179,7 +208,12 @@ class CompletionTest extends TestCase
'TestClass', 'TestClass',
CompletionItemKind::CLASS_, CompletionItemKind::CLASS_,
'TestNamespace', 'TestNamespace',
'Pariatur ut laborum tempor voluptate consequat ea deserunt.', 'Pariatur ut laborum tempor voluptate consequat ea deserunt.' . "\n\n" .
'Deserunt enim minim sunt sint ea nisi. Deserunt excepteur tempor id nostrud' . "\n" .
'laboris commodo ad commodo velit mollit qui non officia id. Nulla duis veniam' . "\n" .
'veniam officia deserunt et non dolore mollit ea quis eiusmod sit non. Occaecat' . "\n" .
'consequat sunt culpa exercitation pariatur id reprehenderit nisi incididunt Lorem' . "\n" .
'sint. Officia culpa pariatur laborum nostrud cupidatat consequat mollit.',
null, null,
null, null,
'TestClass' 'TestClass'
@ -193,6 +227,15 @@ class CompletionTest extends TestCase
null, null,
'\TestNamespace\ChildClass' '\TestNamespace\ChildClass'
), ),
new CompletionItem(
'Example',
CompletionItemKind::CLASS_,
'TestNamespace',
null,
null,
null,
'\TestNamespace\Example'
)
], true), $items); ], true), $items);
} }
@ -204,12 +247,17 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(6, 5) new Position(6, 5)
)->wait(); )->wait();
$this->assertEquals(new CompletionList([ $this->assertCompletionsListSubset(new CompletionList([
new CompletionItem( new CompletionItem(
'TestClass', 'TestClass',
CompletionItemKind::CLASS_, CompletionItemKind::CLASS_,
'TestNamespace', 'TestNamespace',
'Pariatur ut laborum tempor voluptate consequat ea deserunt.' 'Pariatur ut laborum tempor voluptate consequat ea deserunt.' . "\n\n" .
'Deserunt enim minim sunt sint ea nisi. Deserunt excepteur tempor id nostrud' . "\n" .
'laboris commodo ad commodo velit mollit qui non officia id. Nulla duis veniam' . "\n" .
'veniam officia deserunt et non dolore mollit ea quis eiusmod sit non. Occaecat' . "\n" .
'consequat sunt culpa exercitation pariatur id reprehenderit nisi incididunt Lorem' . "\n" .
'sint. Officia culpa pariatur laborum nostrud cupidatat consequat mollit.'
) )
], true), $items); ], true), $items);
} }
@ -222,7 +270,7 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(2, 14) new Position(2, 14)
)->wait(); )->wait();
$this->assertEquals(new CompletionList([ $this->assertCompletionsListSubset(new CompletionList([
new CompletionItem( new CompletionItem(
'staticTestProperty', 'staticTestProperty',
CompletionItemKind::PROPERTY, CompletionItemKind::PROPERTY,
@ -243,7 +291,7 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(2, 11) new Position(2, 11)
)->wait(); )->wait();
$this->assertEquals(new CompletionList([ $this->assertCompletionsListSubset(new CompletionList([
new CompletionItem( new CompletionItem(
'TEST_CLASS_CONST', 'TEST_CLASS_CONST',
CompletionItemKind::VARIABLE, CompletionItemKind::VARIABLE,
@ -276,7 +324,7 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(2, 13) new Position(2, 13)
)->wait(); )->wait();
$this->assertEquals(new CompletionList([ $this->assertCompletionsListSubset(new CompletionList([
new CompletionItem( new CompletionItem(
'TEST_CLASS_CONST', 'TEST_CLASS_CONST',
CompletionItemKind::VARIABLE, CompletionItemKind::VARIABLE,
@ -309,7 +357,7 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(2, 13) new Position(2, 13)
)->wait(); )->wait();
$this->assertEquals(new CompletionList([ $this->assertCompletionsListSubset(new CompletionList([
new CompletionItem( new CompletionItem(
'TEST_CLASS_CONST', 'TEST_CLASS_CONST',
CompletionItemKind::VARIABLE, CompletionItemKind::VARIABLE,
@ -342,12 +390,17 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(6, 6) new Position(6, 6)
)->wait(); )->wait();
$this->assertEquals(new CompletionList([ $this->assertCompletionsListSubset(new CompletionList([
new CompletionItem( new CompletionItem(
'TestClass', 'TestClass',
CompletionItemKind::CLASS_, CompletionItemKind::CLASS_,
null, null,
'Pariatur ut laborum tempor voluptate consequat ea deserunt.', 'Pariatur ut laborum tempor voluptate consequat ea deserunt.' . "\n\n" .
'Deserunt enim minim sunt sint ea nisi. Deserunt excepteur tempor id nostrud' . "\n" .
'laboris commodo ad commodo velit mollit qui non officia id. Nulla duis veniam' . "\n" .
'veniam officia deserunt et non dolore mollit ea quis eiusmod sit non. Occaecat' . "\n" .
'consequat sunt culpa exercitation pariatur id reprehenderit nisi incididunt Lorem' . "\n" .
'sint. Officia culpa pariatur laborum nostrud cupidatat consequat mollit.',
null, null,
null, null,
'TestClass' 'TestClass'
@ -363,9 +416,9 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(2, 1) new Position(2, 1)
)->wait(); )->wait();
$this->assertEquals(new CompletionList([ $this->assertCompletionsListSubset(new CompletionList([
new CompletionItem('class', CompletionItemKind::KEYWORD, null, null, null, null, 'class '), new CompletionItem('class', CompletionItemKind::KEYWORD, null, null, null, null, 'class'),
new CompletionItem('clone', CompletionItemKind::KEYWORD, null, null, null, null, 'clone ') new CompletionItem('clone', CompletionItemKind::KEYWORD, null, null, null, null, 'clone')
], true), $items); ], true), $items);
} }
@ -377,7 +430,7 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(0, 0) new Position(0, 0)
)->wait(); )->wait();
$this->assertEquals(new CompletionList([ $this->assertCompletionsListSubset(new CompletionList([
new CompletionItem( new CompletionItem(
'<?php', '<?php',
CompletionItemKind::KEYWORD, CompletionItemKind::KEYWORD,
@ -391,7 +444,7 @@ class CompletionTest extends TestCase
], true), $items); ], true), $items);
} }
public function testHtmlWithPrefix() public function testHtmlWontBeProposedWithoutCompletionContext()
{ {
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html_with_prefix.php'); $completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html_with_prefix.php');
$this->loader->open($completionUri, file_get_contents($completionUri)); $this->loader->open($completionUri, file_get_contents($completionUri));
@ -399,6 +452,55 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(0, 1) new Position(0, 1)
)->wait(); )->wait();
$this->assertEquals(new CompletionList([], true), $items);
}
public function testHtmlWontBeProposedWithPrefixWithCompletionContext()
{
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html_with_prefix.php');
$this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri),
new Position(0, 1),
new CompletionContext(CompletionTriggerKind::TRIGGER_CHARACTER, '<')
)->wait();
$this->assertEquals(new CompletionList([
new CompletionItem(
'<?php',
CompletionItemKind::KEYWORD,
null,
null,
null,
null,
null,
new TextEdit(new Range(new Position(0, 1), new Position(0, 1)), '?php')
)
], true), $items);
}
public function testHtmlPrefixShouldNotTriggerCompletion()
{
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html_no_completion.php');
$this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri),
new Position(0, 1),
new CompletionContext(CompletionTriggerKind::TRIGGER_CHARACTER, '>')
)->wait();
$this->assertEquals(new CompletionList([], true), $items);
}
public function testHtmlPrefixShouldTriggerCompletionIfManuallyInvoked()
{
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html_no_completion.php');
$this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri),
new Position(0, 1),
new CompletionContext(CompletionTriggerKind::INVOKED)
)->wait();
$this->assertEquals(new CompletionList([ $this->assertEquals(new CompletionList([
new CompletionItem( new CompletionItem(
'<?php', '<?php',
@ -421,7 +523,7 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(4, 6) new Position(4, 6)
)->wait(); )->wait();
$this->assertEquals(new CompletionList([ $this->assertCompletionsListSubset(new CompletionList([
new CompletionItem( new CompletionItem(
'SomeNamespace', 'SomeNamespace',
CompletionItemKind::MODULE, CompletionItemKind::MODULE,
@ -442,7 +544,7 @@ class CompletionTest extends TestCase
new TextDocumentIdentifier($completionUri), new TextDocumentIdentifier($completionUri),
new Position(4, 8) new Position(4, 8)
)->wait(); )->wait();
$this->assertEquals(new CompletionList([ $this->assertCompletionsListSubset(new CompletionList([
new CompletionItem( new CompletionItem(
'$abc2', '$abc2',
CompletionItemKind::VARIABLE, CompletionItemKind::VARIABLE,
@ -465,4 +567,323 @@ class CompletionTest extends TestCase
) )
], true), $items); ], true), $items);
} }
/**
* @dataProvider foreachProvider
*/
public function testForeach(Position $position, array $expectedItems)
{
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/foreach.php');
$this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri),
$position
)->wait();
$this->assertCompletionsListSubset(new CompletionList($expectedItems, true), $items);
}
public function foreachProvider(): array
{
return [
'foreach value' => [
new Position(18, 6),
[
new CompletionItem(
'$value',
CompletionItemKind::VARIABLE,
'\\Foo\\Bar',
null,
null,
null,
null,
new TextEdit(new Range(new Position(18, 6), new Position(18, 6)), 'alue')
),
]
],
'foreach value resolved' => [
new Position(19, 12),
[
new CompletionItem(
'foo',
CompletionItemKind::PROPERTY,
'mixed'
),
new CompletionItem(
'test',
CompletionItemKind::METHOD,
'\\Foo\\Bar[]'
),
]
],
'array creation with multiple objects' => [
new Position(23, 5),
[
new CompletionItem(
'$value',
CompletionItemKind::VARIABLE,
'\\Foo\\Bar|\\stdClass',
null,
null,
null,
null,
new TextEdit(new Range(new Position(23, 5), new Position(23, 5)), 'value')
),
new CompletionItem(
'$key',
CompletionItemKind::VARIABLE,
'int',
null,
null,
null,
null,
new TextEdit(new Range(new Position(23, 5), new Position(23, 5)), 'key')
),
]
],
'array creation with string/int keys and object values' => [
new Position(27, 5),
[
new CompletionItem(
'$value',
CompletionItemKind::VARIABLE,
'\\Foo\\Bar',
null,
null,
null,
null,
new TextEdit(new Range(new Position(27, 5), new Position(27, 5)), 'value')
),
new CompletionItem(
'$key',
CompletionItemKind::VARIABLE,
'string|int',
null,
null,
null,
null,
new TextEdit(new Range(new Position(27, 5), new Position(27, 5)), 'key')
),
]
],
'array creation with only string keys' => [
new Position(31, 5),
[
new CompletionItem(
'$value',
CompletionItemKind::VARIABLE,
'\\Foo\\Bar',
null,
null,
null,
null,
new TextEdit(new Range(new Position(31, 5), new Position(31, 5)), 'value')
),
new CompletionItem(
'$key',
CompletionItemKind::VARIABLE,
'string',
null,
null,
null,
null,
new TextEdit(new Range(new Position(31, 5), new Position(31, 5)), 'key')
),
]
],
'foreach function call' => [
new Position(35, 5),
[
new CompletionItem(
'$value',
CompletionItemKind::VARIABLE,
'\\Foo\\Bar',
null,
null,
null,
null,
new TextEdit(new Range(new Position(35, 5), new Position(35, 5)), 'value')
),
]
],
'foreach unknown type' => [
new Position(39, 10),
[
new CompletionItem(
'$unknown',
CompletionItemKind::VARIABLE,
'mixed',
null,
null,
null,
null,
new TextEdit(new Range(new Position(39, 10), new Position(39, 10)), 'wn')
),
]
],
];
}
public function testMethodReturnType()
{
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/method_return_type.php');
$this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri),
new Position(10, 6)
)->wait();
$this->assertCompletionsListSubset(new CompletionList([
new CompletionItem(
'foo',
CompletionItemKind::METHOD,
'\FooClass',
null,
null,
null,
null,
null
)
], true), $items);
}
public function testStaticMethodReturnType()
{
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/static_method_return_type.php');
$this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri),
new Position(11, 6)
)->wait();
$this->assertCompletionsListSubset(new CompletionList([
new CompletionItem(
'bar',
CompletionItemKind::METHOD,
'mixed',
null,
null,
null,
null,
null
)
], true), $items);
}
private function assertCompletionsListSubset(CompletionList $subsetList, CompletionList $list)
{
foreach ($subsetList->items as $expectedItem) {
$this->assertContains($expectedItem, $list->items, null, null, false);
}
$this->assertEquals($subsetList->isIncomplete, $list->isIncomplete);
}
public function testThisWithoutPrefix()
{
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/this.php');
$this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri),
new Position(12, 15)
)->wait();
$this->assertEquals(new CompletionList([
new CompletionItem(
'foo',
CompletionItemKind::PROPERTY,
'mixed', // Type of the property
null
),
new CompletionItem(
'bar',
CompletionItemKind::PROPERTY,
'mixed', // Type of the property
null
),
new CompletionItem(
'method',
CompletionItemKind::METHOD,
'mixed', // Return type of the method
null
),
new CompletionItem(
'test',
CompletionItemKind::METHOD,
'mixed', // Return type of the method
null
)
], true), $items);
}
public function testThisWithPrefix()
{
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/this_with_prefix.php');
$this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri),
new Position(12, 16)
)->wait();
$this->assertEquals(new CompletionList([
new CompletionItem(
'testProperty',
CompletionItemKind::PROPERTY,
'\TestClass', // Type of the property
'Reprehenderit magna velit mollit ipsum do.'
),
new CompletionItem(
'testMethod',
CompletionItemKind::METHOD,
'\TestClass', // Return type of the method
'Non culpa nostrud mollit esse sunt laboris in irure ullamco cupidatat amet.'
),
new CompletionItem(
'foo',
CompletionItemKind::PROPERTY,
'mixed', // Type of the property
null
),
new CompletionItem(
'bar',
CompletionItemKind::PROPERTY,
'mixed', // Type of the property
null
),
new CompletionItem(
'method',
CompletionItemKind::METHOD,
'mixed', // Return type of the method
null
),
new CompletionItem(
'test',
CompletionItemKind::METHOD,
'mixed', // Return type of the method
null
)
], true), $items);
}
public function testThisReturnValue()
{
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/this_return_value.php');
$this->loader->open($completionUri, file_get_contents($completionUri));
$items = $this->textDocument->completion(
new TextDocumentIdentifier($completionUri),
new Position(17, 23)
)->wait();
$this->assertEquals(new CompletionList([
new CompletionItem(
'foo',
CompletionItemKind::METHOD,
'$this' // Return type of the method
),
new CompletionItem(
'bar',
CompletionItemKind::METHOD,
'mixed' // Return type of the method
),
new CompletionItem(
'qux',
CompletionItemKind::METHOD,
'mixed' // Return type of the method
)
], true), $items);
}
} }

View File

@ -5,11 +5,12 @@ namespace LanguageServer\Tests\Server\TextDocument\Definition;
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\Tests\Server\ServerTestCase; use LanguageServer\Tests\Server\ServerTestCase;
use LanguageServer\{Server, LanguageClient, PhpDocumentLoader, DefinitionResolver}; use LanguageServer\{
Server, LanguageClient, PhpDocumentLoader, DefinitionResolver
};
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Protocol\{TextDocumentIdentifier, Position, Range, Location, ClientCapabilities}; use LanguageServer\Protocol\{TextDocumentIdentifier, Position, Range, Location};
use Sabre\Event\Promise;
class GlobalFallbackTest extends ServerTestCase class GlobalFallbackTest extends ServerTestCase
{ {

View File

@ -24,16 +24,28 @@ class GlobalTest extends ServerTestCase
// namespace keyword // namespace keyword
$result = $this->textDocument->definition( $result = $this->textDocument->definition(
new TextDocumentIdentifier(pathToUri(realpath(__DIR__ . '/../../../../fixtures/references.php'))), new TextDocumentIdentifier(pathToUri(realpath(__DIR__ . '/../../../../fixtures/references.php'))),
new Position(2, 4) new Position(1, 0)
)->wait(); )->wait();
$this->assertEquals([], $result); $this->assertEquals([], $result);
} }
public function testDefinitionForSelfKeyword()
{
// echo self::TEST_CLASS_CONST;
// Get definition for self
$reference = $this->getReferenceLocations('TestClass')[0];
$result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri),
$reference->range->start
)->wait();
$this->assertEquals($this->getDefinitionLocation('TestClass'), $result);
}
public function testDefinitionForClassLike() public function testDefinitionForClassLike()
{ {
// $obj = new TestClass(); // $obj = new TestClass();
// Get definition for TestClass // Get definition for TestClass
$reference = $this->getReferenceLocations('TestClass')[0]; $reference = $this->getReferenceLocations('TestClass')[1];
$result = $this->textDocument->definition( $result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri), new TextDocumentIdentifier($reference->uri),
$reference->range->start $reference->range->start
@ -45,7 +57,7 @@ class GlobalTest extends ServerTestCase
{ {
// TestClass::staticTestMethod(); // TestClass::staticTestMethod();
// Get definition for TestClass // Get definition for TestClass
$reference = $this->getReferenceLocations('TestClass')[1]; $reference = $this->getReferenceLocations('TestClass')[2];
$result = $this->textDocument->definition( $result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri), new TextDocumentIdentifier($reference->uri),
$reference->range->start $reference->range->start
@ -57,7 +69,7 @@ class GlobalTest extends ServerTestCase
{ {
// echo TestClass::$staticTestProperty; // echo TestClass::$staticTestProperty;
// Get definition for TestClass // Get definition for TestClass
$reference = $this->getReferenceLocations('TestClass')[2]; $reference = $this->getReferenceLocations('TestClass')[3];
$result = $this->textDocument->definition( $result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri), new TextDocumentIdentifier($reference->uri),
$reference->range->start $reference->range->start
@ -69,7 +81,7 @@ class GlobalTest extends ServerTestCase
{ {
// TestClass::TEST_CLASS_CONST; // TestClass::TEST_CLASS_CONST;
// Get definition for TestClass // Get definition for TestClass
$reference = $this->getReferenceLocations('TestClass')[3]; $reference = $this->getReferenceLocations('TestClass')[4];
$result = $this->textDocument->definition( $result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri), new TextDocumentIdentifier($reference->uri),
$reference->range->start $reference->range->start
@ -213,7 +225,7 @@ class GlobalTest extends ServerTestCase
{ {
// function whatever(TestClass $param) { // function whatever(TestClass $param) {
// Get definition for TestClass // Get definition for TestClass
$reference = $this->getReferenceLocations('TestClass')[4]; $reference = $this->getReferenceLocations('TestClass')[5];
$result = $this->textDocument->definition( $result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri), new TextDocumentIdentifier($reference->uri),
$reference->range->start $reference->range->start
@ -225,7 +237,7 @@ class GlobalTest extends ServerTestCase
{ {
// function whatever(TestClass $param): TestClass { // function whatever(TestClass $param): TestClass {
// Get definition for TestClass // Get definition for TestClass
$reference = $this->getReferenceLocations('TestClass')[5]; $reference = $this->getReferenceLocations('TestClass')[6];
$result = $this->textDocument->definition( $result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri), new TextDocumentIdentifier($reference->uri),
$reference->range->start $reference->range->start

View File

@ -34,7 +34,7 @@ class NamespacedTest extends GlobalTest
{ {
// use TestNamespace\TestClass; // use TestNamespace\TestClass;
// Get definition for TestClass // Get definition for TestClass
$reference = $this->getReferenceLocations('TestClass')[6]; $reference = $this->getReferenceLocations('TestClass')[7];
$result = $this->textDocument->definition( $result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri), new TextDocumentIdentifier($reference->uri),
$reference->range->start $reference->range->start
@ -46,7 +46,7 @@ class NamespacedTest extends GlobalTest
{ {
// use TestNamespace\{TestTrait, TestInterface}; // use TestNamespace\{TestTrait, TestInterface};
// Get definition for TestInterface // Get definition for TestInterface
$reference = $this->getReferenceLocations('TestClass')[0]; $reference = $this->getReferenceLocations('TestClass')[1];
$result = $this->textDocument->definition( $result = $this->textDocument->definition(
new TextDocumentIdentifier($reference->uri), new TextDocumentIdentifier($reference->uri),
$reference->range->start $reference->range->start

View File

@ -5,17 +5,16 @@ namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, Client, LanguageClient, PhpDocumentLoader, DefinitionResolver}; use LanguageServer\{
Server, LanguageClient, PhpDocumentLoader, DefinitionResolver
};
use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\Protocol\{ use LanguageServer\Protocol\{
TextDocumentIdentifier,
TextDocumentItem,
VersionedTextDocumentIdentifier, VersionedTextDocumentIdentifier,
TextDocumentContentChangeEvent, TextDocumentContentChangeEvent,
Range, Range,
Position, Position
ClientCapabilities
}; };
class DidChangeTest extends TestCase class DidChangeTest extends TestCase

View File

@ -5,11 +5,12 @@ namespace LanguageServer\Tests\Server\TextDocument;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use LanguageServer\Tests\MockProtocolStream; use LanguageServer\Tests\MockProtocolStream;
use LanguageServer\{Server, Client, LanguageClient, PhpDocumentLoader, DefinitionResolver}; use LanguageServer\{
Server, LanguageClient, PhpDocumentLoader, DefinitionResolver
};
use LanguageServer\ContentRetriever\FileSystemContentRetriever; use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex}; use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
use LanguageServer\Protocol\{TextDocumentItem, TextDocumentIdentifier, ClientCapabilities}; use LanguageServer\Protocol\{TextDocumentItem, TextDocumentIdentifier};
use Exception;
class DidCloseTest extends TestCase class DidCloseTest extends TestCase
{ {

View File

@ -18,18 +18,21 @@ class DocumentSymbolTest extends ServerTestCase
$result = $this->textDocument->documentSymbol(new TextDocumentIdentifier($uri))->wait(); $result = $this->textDocument->documentSymbol(new TextDocumentIdentifier($uri))->wait();
// @codingStandardsIgnoreStart // @codingStandardsIgnoreStart
$this->assertEquals([ $this->assertEquals([
new SymbolInformation('TestNamespace', SymbolKind::NAMESPACE, $this->getDefinitionLocation('TestNamespace'), ''), new SymbolInformation('TestNamespace', SymbolKind::NAMESPACE, $this->getDefinitionLocation('TestNamespace'), ''),
new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TEST_CONST'), 'TestNamespace'), new SymbolInformation('TEST_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TEST_CONST'), 'TestNamespace'),
new SymbolInformation('TestClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestClass'), 'TestNamespace'), new SymbolInformation('TestClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestClass'), 'TestNamespace'),
new SymbolInformation('TEST_CLASS_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TestClass::TEST_CLASS_CONST'), 'TestNamespace\\TestClass'), new SymbolInformation('TEST_CLASS_CONST', SymbolKind::CONSTANT, $this->getDefinitionLocation('TestNamespace\\TestClass::TEST_CLASS_CONST'), 'TestNamespace\\TestClass'),
new SymbolInformation('staticTestProperty', SymbolKind::PROPERTY, $this->getDefinitionLocation('TestNamespace\\TestClass::staticTestProperty'), 'TestNamespace\\TestClass'), new SymbolInformation('staticTestProperty', SymbolKind::PROPERTY, $this->getDefinitionLocation('TestNamespace\\TestClass::staticTestProperty'), 'TestNamespace\\TestClass'),
new SymbolInformation('testProperty', SymbolKind::PROPERTY, $this->getDefinitionLocation('TestNamespace\\TestClass::testProperty'), 'TestNamespace\\TestClass'), new SymbolInformation('testProperty', SymbolKind::PROPERTY, $this->getDefinitionLocation('TestNamespace\\TestClass::testProperty'), 'TestNamespace\\TestClass'),
new SymbolInformation('staticTestMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestNamespace\\TestClass::staticTestMethod()'), 'TestNamespace\\TestClass'), new SymbolInformation('staticTestMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestNamespace\\TestClass::staticTestMethod()'), 'TestNamespace\\TestClass'),
new SymbolInformation('testMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestNamespace\\TestClass::testMethod()'), 'TestNamespace\\TestClass'), new SymbolInformation('testMethod', SymbolKind::METHOD, $this->getDefinitionLocation('TestNamespace\\TestClass::testMethod()'), 'TestNamespace\\TestClass'),
new SymbolInformation('TestTrait', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestTrait'), 'TestNamespace'), new SymbolInformation('TestTrait', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\TestTrait'), 'TestNamespace'),
new SymbolInformation('TestInterface', SymbolKind::INTERFACE, $this->getDefinitionLocation('TestNamespace\\TestInterface'), 'TestNamespace'), new SymbolInformation('TestInterface', SymbolKind::INTERFACE, $this->getDefinitionLocation('TestNamespace\\TestInterface'), 'TestNamespace'),
new SymbolInformation('test_function', SymbolKind::FUNCTION, $this->getDefinitionLocation('TestNamespace\\test_function()'), 'TestNamespace'), new SymbolInformation('test_function', SymbolKind::FUNCTION, $this->getDefinitionLocation('TestNamespace\\test_function()'), 'TestNamespace'),
new SymbolInformation('ChildClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\ChildClass'), 'TestNamespace'), new SymbolInformation('ChildClass', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\ChildClass'), 'TestNamespace'),
new SymbolInformation('Example', SymbolKind::CLASS_, $this->getDefinitionLocation('TestNamespace\\Example'), 'TestNamespace'),
new SymbolInformation('__construct', SymbolKind::CONSTRUCTOR, $this->getDefinitionLocation('TestNamespace\\Example::__construct'), 'TestNamespace\\Example'),
new SymbolInformation('__destruct', SymbolKind::CONSTRUCTOR, $this->getDefinitionLocation('TestNamespace\\Example::__destruct'), 'TestNamespace\\Example')
], $result); ], $result);
// @codingStandardsIgnoreEnd // @codingStandardsIgnoreEnd
} }

Some files were not shown because too many files have changed in this diff Show More