Merge remote-tracking branch 'upstream/master' into feature/allow-configurable-file-extension-for-indexing
commit
9cc2736df2
|
@ -7,3 +7,4 @@ fixtures/
|
|||
coverage/
|
||||
coverage.xml
|
||||
images/
|
||||
node_modules/
|
||||
|
|
|
@ -7,7 +7,7 @@ trim_trailing_whitespace = true
|
|||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.json,*.yml]
|
||||
[*.{json,yml}]
|
||||
indent_size = 2
|
||||
|
||||
[composer.json]
|
||||
|
|
|
@ -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
|
|
@ -1,7 +1,8 @@
|
|||
.DS_Store
|
||||
.vscode
|
||||
.idea
|
||||
vendor/
|
||||
.phpls/
|
||||
composer.lock
|
||||
stubs
|
||||
*.ast
|
||||
node_modules/
|
||||
|
|
|
@ -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
|
57
.travis.yml
57
.travis.yml
|
@ -2,27 +2,56 @@ language: php
|
|||
|
||||
php:
|
||||
- '7.0'
|
||||
- '7.2'
|
||||
|
||||
services:
|
||||
- docker
|
||||
git:
|
||||
depth: 10
|
||||
submodules: false
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/Library/Caches/Homebrew
|
||||
- $HOME/.composer/cache
|
||||
- $HOME/.npm
|
||||
|
||||
install:
|
||||
- composer install
|
||||
- composer run-script parse-stubs
|
||||
|
||||
- composer install --prefer-dist --no-interaction
|
||||
script:
|
||||
- vendor/bin/phpcs -n
|
||||
- vendor/bin/phpunit --coverage-clover=coverage.xml
|
||||
|
||||
after_success:
|
||||
- vendor/bin/phpunit --coverage-clover=coverage.xml --colors=always
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
- |
|
||||
if [[ $TRAVIS_TAG == v* ]]; then
|
||||
docker build -t felixfbecker/php-language-server:${TRAVIS_TAG:1} .
|
||||
docker login -e="$DOCKER_EMAIL" -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
|
||||
docker push felixfbecker/php-language-server:${TRAVIS_TAG:1}
|
||||
fi
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- stage: test
|
||||
os: osx
|
||||
osx_image: xcode9.1
|
||||
language: generic
|
||||
before_install:
|
||||
# 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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
// Place your settings in this file to overwrite default and user settings.
|
||||
{
|
||||
"search.exclude": {
|
||||
"**/validation": true,
|
||||
"**/tests/Validation/cases": true
|
||||
}
|
||||
}
|
|
@ -7,11 +7,6 @@
|
|||
FROM php:7-cli
|
||||
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-install pcntl
|
||||
COPY ./php.ini /usr/local/etc/php/conf.d/
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
47
README.md
47
README.md
|
@ -1,9 +1,11 @@
|
|||
# PHP Language Server
|
||||
|
||||
[](https://packagist.org/packages/felixfbecker/language-server)
|
||||
[](https://travis-ci.org/felixfbecker/php-language-server)
|
||||
[](https://travis-ci.org/felixfbecker/php-language-server)
|
||||
[](https://ci.appveyor.com/project/felixfbecker/php-language-server/branch/master)
|
||||
[](https://codecov.io/gh/felixfbecker/php-language-server)
|
||||
[](https://gemnasium.com/github.com/felixfbecker/php-language-server)
|
||||
[](https://github.com/semantic-release/semantic-release)
|
||||
[](https://php.net/)
|
||||
[](https://github.com/felixfbecker/php-language-server/blob/master/LICENSE.txt)
|
||||
[](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).
|
||||
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)
|
||||
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
|
||||
|
||||
### [Completion](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#textDocument_completion)
|
||||

|
||||
|
||||
### [Signature Help](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#textDocument_signatureHelp)
|
||||

|
||||
|
||||
### [Go To Definition](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#goto-definition-request)
|
||||

|
||||
|
||||
|
@ -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.
|
||||
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)
|
||||

|
||||
|
||||
### Error reporting through [Publish Diagnostics](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#publishdiagnostics-notification)
|
||||

|
||||
|
||||
|
@ -170,7 +185,7 @@ Example:
|
|||
#### `--memory-limit=integer` (optional)
|
||||
Sets memory limit for language server.
|
||||
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:
|
||||
|
||||
|
@ -180,7 +195,8 @@ Example:
|
|||
- [VS Code PHP IntelliSense](https://github.com/felixfbecker/vscode-php-intellisense)
|
||||
- [Eclipse Che](https://eclipse.org/che/)
|
||||
- [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
|
||||
|
||||
|
@ -190,14 +206,21 @@ Clone the repository and run
|
|||
composer install
|
||||
|
||||
to install dependencies.
|
||||
Then parse the stubs with
|
||||
|
||||
composer run-script parse-stubs
|
||||
|
||||
Run the tests with
|
||||
|
||||
vendor/bin/phpunit
|
||||
composer test
|
||||
|
||||
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).
|
||||
|
|
|
@ -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'
|
|
@ -1,12 +1,12 @@
|
|||
<?php
|
||||
|
||||
use LanguageServer\{LanguageServer, ProtocolStreamReader, ProtocolStreamWriter};
|
||||
use LanguageServer\{LanguageServer, ProtocolStreamReader, ProtocolStreamWriter, StderrLogger};
|
||||
use Sabre\Event\Loop;
|
||||
use Composer\{Factory, XdebugHandler};
|
||||
use Composer\XdebugHandler\XdebugHandler;
|
||||
|
||||
$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) {
|
||||
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);
|
||||
});
|
||||
|
||||
$logger = new StderrLogger();
|
||||
|
||||
// Only write uncaught exceptions to STDERR, not STDOUT
|
||||
set_exception_handler(function (\Throwable $e) {
|
||||
fwrite(STDERR, (string)$e);
|
||||
set_exception_handler(function (\Throwable $e) use ($logger) {
|
||||
$logger->critical((string)$e);
|
||||
});
|
||||
|
||||
@cli_set_process_title('PHP Language Server');
|
||||
|
||||
// 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'])) {
|
||||
// Connect to a TCP server
|
||||
$address = $options['tcp'];
|
||||
$socket = stream_socket_client('tcp://' . $address, $errno, $errstr);
|
||||
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);
|
||||
}
|
||||
stream_set_blocking($socket, false);
|
||||
|
@ -53,29 +58,30 @@ if (!empty($options['tcp'])) {
|
|||
$address = $options['tcp-server'];
|
||||
$tcpServer = stream_socket_server('tcp://' . $address, $errno, $errstr);
|
||||
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);
|
||||
}
|
||||
fwrite(STDOUT, "Server listening on $address\n");
|
||||
if (!extension_loaded('pcntl')) {
|
||||
fwrite(STDERR, "PCNTL is not available. Only a single connection will be accepted\n");
|
||||
$logger->debug("Server listening on $address");
|
||||
$pcntlAvailable = extension_loaded('pcntl');
|
||||
if (!$pcntlAvailable) {
|
||||
$logger->notice('PCNTL is not available. Only a single connection will be accepted');
|
||||
}
|
||||
while ($socket = stream_socket_accept($tcpServer, -1)) {
|
||||
fwrite(STDOUT, "Connection accepted\n");
|
||||
$logger->debug('Connection accepted');
|
||||
stream_set_blocking($socket, false);
|
||||
if (extension_loaded('pcntl')) {
|
||||
if ($pcntlAvailable) {
|
||||
// If PCNTL is available, fork a child process for the connection
|
||||
// An exit notification will only terminate the child process
|
||||
$pid = pcntl_fork();
|
||||
if ($pid === -1) {
|
||||
fwrite(STDERR, "Could not fork\n");
|
||||
$logger->critical('Could not fork');
|
||||
exit(1);
|
||||
} else if ($pid === 0) {
|
||||
// Child process
|
||||
$reader = new ProtocolStreamReader($socket);
|
||||
$writer = new ProtocolStreamWriter($socket);
|
||||
$reader->on('close', function () {
|
||||
fwrite(STDOUT, "Connection closed\n");
|
||||
$reader->on('close', function () use ($logger) {
|
||||
$logger->debug('Connection closed');
|
||||
});
|
||||
$ls = new LanguageServer($reader, $writer);
|
||||
Loop\run();
|
||||
|
@ -94,6 +100,7 @@ if (!empty($options['tcp'])) {
|
|||
}
|
||||
} else {
|
||||
// Use STDIO
|
||||
$logger->debug('Listening on STDIN');
|
||||
stream_set_blocking(STDIN, false);
|
||||
$ls = new LanguageServer(
|
||||
new ProtocolStreamReader(STDIN),
|
||||
|
|
|
@ -1,14 +1,7 @@
|
|||
{
|
||||
"name": "felixfbecker/language-server",
|
||||
"description": "PHP Implementation of the Visual Studio Code Language Server Protocol",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Felix Becker",
|
||||
"email": "felix.b@outlook.com"
|
||||
}
|
||||
],
|
||||
"license": "ISC",
|
||||
"type": "library",
|
||||
"keywords": [
|
||||
"php",
|
||||
"language",
|
||||
|
@ -21,33 +14,38 @@
|
|||
"autocompletion",
|
||||
"refactor"
|
||||
],
|
||||
"bin": ["bin/php-language-server.php"],
|
||||
"scripts": {
|
||||
"parse-stubs": "LanguageServer\\ComposerScripts::parseStubs",
|
||||
"post-install-cmd": "@parse-stubs"
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "Felix Becker",
|
||||
"email": "felix.b@outlook.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.0",
|
||||
"nikic/php-parser": "^3.0.4",
|
||||
"phpdocumentor/reflection-docblock": "^3.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",
|
||||
"php": "^7.0",
|
||||
"composer/xdebug-handler": "^1.0",
|
||||
"felixfbecker/advanced-json-rpc": "^3.0.0",
|
||||
"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": {
|
||||
"psr-4": {
|
||||
"LanguageServer\\": "src/"
|
||||
},
|
||||
"files" : [
|
||||
"src/utils.php"
|
||||
"src/utils.php",
|
||||
"src/FqnUtilities.php",
|
||||
"src/ParserHelpers.php"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
|
@ -55,8 +53,19 @@
|
|||
"LanguageServer\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^5.5",
|
||||
"phpunit/php-code-coverage": "^4.0"
|
||||
}
|
||||
"bin": [
|
||||
"bin/php-language-server.php"
|
||||
],
|
||||
"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
|
||||
}
|
||||
|
|
|
@ -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: "
|
|
@ -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();
|
||||
}
|
|
@ -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) {
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace MyNamespace;
|
||||
|
||||
class SomeClass
|
||||
{
|
||||
public function someMethod()
|
||||
{
|
||||
tes
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
class FooClass {
|
||||
public function foo(): FooClass {
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
$fc = new FooClass();
|
||||
$foo = $fc->foo();
|
||||
$foo->
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
class FooClass {
|
||||
public static function staticFoo(): FooClass {
|
||||
return new FooClass();
|
||||
}
|
||||
|
||||
public function bar() { }
|
||||
}
|
||||
|
||||
$foo = FooClass::staticFoo();
|
||||
$foo->
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
class ThisClass
|
||||
{
|
||||
private $foo;
|
||||
private $bar;
|
||||
|
||||
protected function method()
|
||||
{
|
||||
}
|
||||
public function test()
|
||||
{
|
||||
$this->
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
class ThisClassPrefix extends TestClass
|
||||
{
|
||||
private $foo;
|
||||
private $bar;
|
||||
|
||||
protected function method()
|
||||
{
|
||||
}
|
||||
public function test()
|
||||
{
|
||||
$this->m
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
class Foo
|
||||
{
|
||||
public function bar()
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
class Foo
|
||||
{
|
||||
public static function bar()
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -98,3 +98,22 @@ new class {
|
|||
};
|
||||
|
||||
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()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
|
@ -98,3 +98,8 @@ new class {
|
|||
};
|
||||
|
||||
class ChildClass extends TestClass {}
|
||||
|
||||
class Example {
|
||||
public function __construct() {}
|
||||
public function __destruct() {}
|
||||
}
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 158 KiB |
Binary file not shown.
Before Width: | Height: | Size: 51 KiB |
Binary file not shown.
After Width: | Height: | Size: 316 KiB |
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -2,9 +2,12 @@
|
|||
<ruleset name="PHP Language Server">
|
||||
<file>src</file>
|
||||
<file>tests</file>
|
||||
<exclude-pattern>tests/Validation/cases</exclude-pattern>
|
||||
<rule ref="PSR2">
|
||||
<exclude name="PSR2.Namespaces.UseDeclaration.MultipleDeclarations"/>
|
||||
<exclude name="PSR2.ControlStructures.ElseIfDeclaration.NotAllowed"/>
|
||||
<exclude name="PSR2.ControlStructures.ControlStructureSpacing.SpacingAfterOpenBrace"/>
|
||||
<exclude name="Squiz.WhiteSpace.ControlStructureSpacing.SpacingBeforeClose"/>
|
||||
<exclude name="Squiz.WhiteSpace.ControlStructureSpacing.SpacingAfterOpen"/>
|
||||
</rule>
|
||||
</ruleset>
|
||||
|
|
|
@ -1,14 +1,30 @@
|
|||
<?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>
|
||||
<testsuite name="PHP Language Server Test Suite">
|
||||
<directory>./tests</directory>
|
||||
<directory suffix="Test.php">./tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<filter>
|
||||
<whitelist>
|
||||
<directory>./src</directory>
|
||||
<whitelist processUncoveredFilesFromWhitelist="true">
|
||||
<directory suffix=".php">./src</directory>
|
||||
</whitelist>
|
||||
</filter>
|
||||
<php>
|
||||
<ini name="memory_limit" value="1024M"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
|
|
|
@ -3,7 +3,6 @@ declare(strict_types = 1);
|
|||
|
||||
namespace LanguageServer\Cache;
|
||||
|
||||
use LanguageServer\LanguageClient;
|
||||
use Sabre\Event\Promise;
|
||||
|
||||
/**
|
||||
|
|
|
@ -4,7 +4,7 @@ declare(strict_types = 1);
|
|||
namespace LanguageServer\Client;
|
||||
|
||||
use LanguageServer\ClientHandler;
|
||||
use LanguageServer\Protocol\{Message, TextDocumentItem, TextDocumentIdentifier};
|
||||
use LanguageServer\Protocol\{TextDocumentItem, TextDocumentIdentifier};
|
||||
use Sabre\Event\Promise;
|
||||
use JsonMapper;
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ declare(strict_types = 1);
|
|||
namespace LanguageServer\Client;
|
||||
|
||||
use LanguageServer\ClientHandler;
|
||||
use LanguageServer\Protocol\Message;
|
||||
use Sabre\Event\Promise;
|
||||
|
||||
/**
|
||||
|
|
|
@ -4,7 +4,6 @@ declare(strict_types = 1);
|
|||
namespace LanguageServer\Client;
|
||||
|
||||
use LanguageServer\ClientHandler;
|
||||
use LanguageServer\Protocol\Message;
|
||||
use Sabre\Event\Promise;
|
||||
|
||||
/**
|
||||
|
|
|
@ -71,7 +71,6 @@ class ClientHandler
|
|||
*/
|
||||
public function notify(string $method, $params): Promise
|
||||
{
|
||||
$id = $this->idGenerator->generate();
|
||||
return $this->protocolWriter->write(
|
||||
new Protocol\Message(
|
||||
new AdvancedJsonRpc\Notification($method, (object)$params)
|
||||
|
|
|
@ -3,7 +3,6 @@ declare(strict_types = 1);
|
|||
|
||||
namespace LanguageServer;
|
||||
|
||||
use PhpParser\Node;
|
||||
use LanguageServer\Index\ReadableIndex;
|
||||
use LanguageServer\Protocol\{
|
||||
TextEdit,
|
||||
|
@ -11,8 +10,13 @@ use LanguageServer\Protocol\{
|
|||
Position,
|
||||
CompletionList,
|
||||
CompletionItem,
|
||||
CompletionItemKind
|
||||
CompletionItemKind,
|
||||
CompletionContext,
|
||||
CompletionTriggerKind
|
||||
};
|
||||
use Microsoft\PhpParser;
|
||||
use Microsoft\PhpParser\Node;
|
||||
use Generator;
|
||||
|
||||
class CompletionProvider
|
||||
{
|
||||
|
@ -48,6 +52,7 @@ class CompletionProvider
|
|||
'eval',
|
||||
'exit',
|
||||
'extends',
|
||||
'false',
|
||||
'final',
|
||||
'finally',
|
||||
'for',
|
||||
|
@ -66,6 +71,7 @@ class CompletionProvider
|
|||
'list',
|
||||
'namespace',
|
||||
'new',
|
||||
'null',
|
||||
'or',
|
||||
'print',
|
||||
'private',
|
||||
|
@ -78,13 +84,30 @@ class CompletionProvider
|
|||
'switch',
|
||||
'throw',
|
||||
'trait',
|
||||
'true',
|
||||
'try',
|
||||
'unset',
|
||||
'use',
|
||||
'var',
|
||||
'while',
|
||||
'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')
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -117,159 +140,82 @@ class CompletionProvider
|
|||
*
|
||||
* @param PhpDocument $doc The opened document
|
||||
* @param Position $pos The cursor position
|
||||
* @param CompletionContext $context The completion context
|
||||
* @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);
|
||||
|
||||
if ($node instanceof Node\Expr\Error) {
|
||||
$node = $node->getAttribute('parentNode');
|
||||
// Get the node at the position under the cursor
|
||||
$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->isIncomplete = true;
|
||||
|
||||
// A non-free node means we do NOT suggest global symbols
|
||||
if (
|
||||
$node instanceof Node\Expr\MethodCall
|
||||
|| $node instanceof Node\Expr\PropertyFetch
|
||||
|| $node instanceof Node\Expr\StaticCall
|
||||
|| $node instanceof Node\Expr\StaticPropertyFetch
|
||||
|| $node instanceof Node\Expr\ClassConstFetch
|
||||
if ($node instanceof Node\Expression\Variable &&
|
||||
$node->parent instanceof Node\Expression\ObjectCreationExpression &&
|
||||
$node->name instanceof PhpParser\MissingToken
|
||||
) {
|
||||
// If the name is an Error node, just filter by the class
|
||||
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 : ''];
|
||||
$node = $node->parent;
|
||||
}
|
||||
$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) {
|
||||
foreach ($prefixes as $prefix) {
|
||||
if (substr($fqn, 0, strlen($prefix)) === $prefix && !$def->isGlobal) {
|
||||
$list->items[] = CompletionItem::fromDefinition($def);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
// A ConstFetch means any static reference, like a class, interface, etc. or keyword
|
||||
($node instanceof Node\Name && $node->getAttribute('parentNode') instanceof Node\Expr\ConstFetch)
|
||||
|| $node instanceof Node\Expr\New_
|
||||
) {
|
||||
$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) {
|
||||
// Inspect the type of expression under the cursor
|
||||
|
||||
$content = $doc->getContent();
|
||||
$offset = $pos->toOffset($content);
|
||||
if (
|
||||
$def->isGlobal // exclude methods, properties etc.
|
||||
$node === null
|
||||
|| (
|
||||
$node instanceof Node\Statement\InlineHtml
|
||||
&& (
|
||||
!$prefix
|
||||
|| (
|
||||
((!$namespace || $isFullyQualified) && substr($fqn, 0, $prefixLen) === $prefix)
|
||||
|| (
|
||||
$namespace
|
||||
&& !$isFullyQualified
|
||||
&& substr($fqn, 0, $namespacedPrefixLen) === $namespacedPrefix
|
||||
$context !== null
|
||||
// Make sure to not suggest on the > trigger character in HTML
|
||||
&& (
|
||||
$context->triggerKind === CompletionTriggerKind::INVOKED
|
||||
|| $context->triggerCharacter === '<'
|
||||
)
|
||||
)
|
||||
)
|
||||
// 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
|
||||
if ($namespace && ($alias = array_search($def, $aliasedDefs, true)) !== false) {
|
||||
// $alias is the name under which this definition is aliased in the current namespace
|
||||
$item->insertText = $alias;
|
||||
} else if ($namespace && !($prefix && $isFullyQualified)) {
|
||||
// Insert the global FQN with trailing backslash
|
||||
$item->insertText = '\\' . $fqn;
|
||||
} else {
|
||||
// Insert the FQN without trailing backlash
|
||||
$item->insertText = $fqn;
|
||||
}
|
||||
// HTML, beginning of file
|
||||
|
||||
// Inside HTML and at the beginning of the file, propose <?php
|
||||
$item = new CompletionItem('<?php', CompletionItemKind::KEYWORD);
|
||||
$item->textEdit = new TextEdit(
|
||||
new Range($pos, $pos),
|
||||
stripStringOverlap($doc->getRange(new Range(new Position(0, 0), $pos)), '<?php')
|
||||
);
|
||||
$list->items[] = $item;
|
||||
}
|
||||
}
|
||||
// Suggest keywords
|
||||
if ($node instanceof Node\Name && $node->getAttribute('parentNode') instanceof Node\Expr\ConstFetch) {
|
||||
foreach (self::KEYWORDS as $keyword) {
|
||||
if (substr($keyword, 0, $prefixLen) === $prefix) {
|
||||
$item = new CompletionItem($keyword, CompletionItemKind::KEYWORD);
|
||||
$item->insertText = $keyword . ' ';
|
||||
$list->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} elseif (
|
||||
$node instanceof Node\Expr\Variable
|
||||
|| ($node && $node->getAttribute('parentNode') instanceof Node\Expr\Variable)
|
||||
$node instanceof Node\Expression\Variable
|
||||
&& !(
|
||||
$node->parent instanceof Node\Expression\ScopedPropertyAccessExpression
|
||||
&& $node->parent->memberName === $node
|
||||
)
|
||||
) {
|
||||
// Variables
|
||||
//
|
||||
// $|
|
||||
// $a|
|
||||
|
||||
// Find variables, parameters and use statements in the scope
|
||||
// If there was only a $ typed, $node will be instanceof Node\Error
|
||||
$namePrefix = $node instanceof Node\Expr\Variable && is_string($node->name) ? $node->name : '';
|
||||
$namePrefix = $node->getName() ?? '';
|
||||
foreach ($this->suggestVariablesAtNode($node, $namePrefix) as $var) {
|
||||
$item = new CompletionItem;
|
||||
$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->detail = (string)$this->definitionResolver->getTypeFromNode($var);
|
||||
$item->textEdit = new TextEdit(
|
||||
|
@ -278,36 +224,205 @@ class CompletionProvider
|
|||
);
|
||||
$list->items[] = $item;
|
||||
}
|
||||
} else if ($node instanceof Node\Stmt\InlineHTML || $pos == new Position(0, 0)) {
|
||||
$item = new CompletionItem('<?php', CompletionItemKind::KEYWORD);
|
||||
$item->textEdit = new TextEdit(
|
||||
new Range($pos, $pos),
|
||||
stripStringOverlap($doc->getRange(new Range(new Position(0, 0), $pos)), '<?php')
|
||||
|
||||
} elseif ($node instanceof Node\Expression\MemberAccessExpression) {
|
||||
// Member access expressions
|
||||
//
|
||||
// $a->c|
|
||||
// $a->|
|
||||
|
||||
// Multiple prefixes for all possible types
|
||||
$fqns = FqnUtilities\getFqnsFromType(
|
||||
$this->definitionResolver->resolveExpressionNodeToType($node->dereferencableExpression)
|
||||
);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return string[]
|
||||
* @return Generator
|
||||
*/
|
||||
private function expandParentFqns(array $fqns): array
|
||||
private function expandParentFqns(array $fqns) : Generator
|
||||
{
|
||||
$expanded = $fqns;
|
||||
foreach ($fqns as $fqn) {
|
||||
yield $fqn;
|
||||
$def = $this->index->getDefinition($fqn);
|
||||
if ($def) {
|
||||
foreach ($this->expandParentFqns($def->extends) as $parent) {
|
||||
$expanded[] = $parent;
|
||||
if ($def !== null) {
|
||||
foreach ($def->getAncestorDefinitions($this->index) as $name => $def) {
|
||||
yield $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $expanded;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -335,30 +450,34 @@ class CompletionProvider
|
|||
|
||||
// Walk the AST upwards until a scope boundary is met
|
||||
$level = $node;
|
||||
while ($level && !($level instanceof Node\FunctionLike)) {
|
||||
while ($level && !($level instanceof PhpParser\FunctionLike)) {
|
||||
// Walk siblings before the node
|
||||
$sibling = $level;
|
||||
while ($sibling = $sibling->getAttribute('previousSibling')) {
|
||||
while ($sibling = $sibling->getPreviousSibling()) {
|
||||
// Collect all variables inside the sibling node
|
||||
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,
|
||||
// also add its parameters and closure uses to the result list
|
||||
if ($level instanceof Node\FunctionLike) {
|
||||
foreach ($level->params as $param) {
|
||||
if (!isset($vars[$param->name]) && substr($param->name, 0, strlen($namePrefix)) === $namePrefix) {
|
||||
$vars[$param->name] = $param;
|
||||
if ($level && $level instanceof PhpParser\FunctionLike && $level->parameters !== null) {
|
||||
foreach ($level->parameters->getValues() as $param) {
|
||||
$paramName = $param->getName();
|
||||
if (empty($namePrefix) || strpos($paramName, $namePrefix) !== false) {
|
||||
$vars[$paramName] = $param;
|
||||
}
|
||||
}
|
||||
if ($level instanceof Node\Expr\Closure) {
|
||||
foreach ($level->uses as $use) {
|
||||
if (!isset($vars[$use->var]) && substr($use->var, 0, strlen($namePrefix)) === $namePrefix) {
|
||||
$vars[$use->var] = $use;
|
||||
|
||||
if ($level instanceof Node\Expression\AnonymousFunctionCreationExpression && $level->anonymousFunctionUseClause !== null &&
|
||||
$level->anonymousFunctionUseClause->useVariableNameList !== null) {
|
||||
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 string $namePrefix Prefix to filter
|
||||
* @return Node\Expr\Variable[]
|
||||
* @return Node\Expression\Variable[]
|
||||
*/
|
||||
private function findVariableDefinitionsInNode(Node $node, string $namePrefix = ''): array
|
||||
{
|
||||
$vars = [];
|
||||
// If the child node is a variable assignment, save it
|
||||
$parent = $node->getAttribute('parentNode');
|
||||
if (
|
||||
$node instanceof Node\Expr\Variable
|
||||
&& ($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
|
||||
|
||||
$isAssignmentToVariable = function ($node) {
|
||||
return $node instanceof Node\Expression\AssignmentExpression;
|
||||
};
|
||||
|
||||
if ($this->isAssignmentToVariableWithPrefix($node, $namePrefix)) {
|
||||
$vars[] = $node->leftOperand;
|
||||
} elseif ($node instanceof Node\ForeachKey || $node instanceof Node\ForeachValue) {
|
||||
foreach ($node->getDescendantNodes() as $descendantNode) {
|
||||
if ($descendantNode instanceof Node\Expression\Variable
|
||||
&& ($namePrefix === '' || strpos($descendantNode->getName(), $namePrefix) !== false)
|
||||
) {
|
||||
$vars[] = $node;
|
||||
$vars[] = $descendantNode;
|
||||
}
|
||||
// Iterate over subnodes
|
||||
foreach ($node->getSubNodeNames() as $attr) {
|
||||
if (!isset($node->$attr)) {
|
||||
continue;
|
||||
}
|
||||
$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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ use phpDocumentor\Reflection\DocBlockFactory;
|
|||
use Webmozart\PathUtil\Path;
|
||||
use Sabre\Uri;
|
||||
use function Sabre\Event\coroutine;
|
||||
use Microsoft\PhpParser;
|
||||
|
||||
foreach ([__DIR__ . '/../../../autoload.php', __DIR__ . '/../autoload.php', __DIR__ . '/../vendor/autoload.php'] as $file) {
|
||||
if (file_exists($file)) {
|
||||
|
@ -29,7 +30,7 @@ class ComposerScripts
|
|||
$finder = new FileSystemFilesFinder;
|
||||
$contentRetriever = new FileSystemContentRetriever;
|
||||
$docBlockFactory = DocBlockFactory::createInstance();
|
||||
$parser = new Parser;
|
||||
$parser = new PhpParser\Parser();
|
||||
$definitionResolver = new DefinitionResolver($index);
|
||||
|
||||
$stubsLocation = null;
|
||||
|
@ -55,7 +56,8 @@ class ComposerScripts
|
|||
$parts['scheme'] = 'phpstubs';
|
||||
$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();
|
||||
|
|
|
@ -3,10 +3,10 @@ declare(strict_types = 1);
|
|||
|
||||
namespace LanguageServer;
|
||||
|
||||
use PhpParser\Node;
|
||||
use LanguageServer\Index\ReadableIndex;
|
||||
use phpDocumentor\Reflection\{Types, Type, Fqsen, TypeResolver};
|
||||
use LanguageServer\Protocol\SymbolInformation;
|
||||
use Exception;
|
||||
use Generator;
|
||||
|
||||
/**
|
||||
* Class used to represent symbols
|
||||
|
@ -38,12 +38,20 @@ class Definition
|
|||
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
|
||||
*
|
||||
* @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
|
||||
|
@ -60,7 +68,7 @@ class Definition
|
|||
public $canBeInstantiated;
|
||||
|
||||
/**
|
||||
* @var Protocol\SymbolInformation
|
||||
* @var SymbolInformation
|
||||
*/
|
||||
public $symbolInformation;
|
||||
|
||||
|
@ -70,7 +78,7 @@ class Definition
|
|||
* For functions and methods, this is the return type.
|
||||
* For any other declaration it will be null.
|
||||
* 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
|
||||
*/
|
||||
|
@ -89,4 +97,40 @@ class Definition
|
|||
* @var string
|
||||
*/
|
||||
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
|
@ -22,7 +22,11 @@ class FileSystemFilesFinder implements FilesFinder
|
|||
return coroutine(function () use ($glob) {
|
||||
$uris = [];
|
||||
foreach (new GlobIterator($glob) as $path) {
|
||||
// Exclude any directories that also match the glob pattern
|
||||
if (!is_dir($path)) {
|
||||
$uris[] = pathToUri($path);
|
||||
}
|
||||
|
||||
yield timeout();
|
||||
}
|
||||
return $uris;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -117,7 +117,7 @@ class Index implements ReadableIndex, \Serializable
|
|||
* Registers a definition
|
||||
*
|
||||
* @param string $fqn The fully qualified name of the symbol
|
||||
* @param string $definition The Definition object
|
||||
* @param Definition $definition The Definition object
|
||||
* @return void
|
||||
*/
|
||||
public function setDefinition(string $fqn, Definition $definition)
|
||||
|
@ -150,6 +150,17 @@ class Index implements ReadableIndex, \Serializable
|
|||
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
|
||||
*
|
||||
|
|
|
@ -8,16 +8,15 @@ use LanguageServer\FilesFinder\FilesFinder;
|
|||
use LanguageServer\Index\{DependenciesIndex, Index};
|
||||
use LanguageServer\Protocol\MessageType;
|
||||
use Webmozart\PathUtil\Path;
|
||||
use Composer\Semver\VersionParser;
|
||||
use Sabre\Event\Promise;
|
||||
use function Sabre\Event\coroutine;
|
||||
|
||||
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
|
||||
|
@ -156,7 +155,7 @@ class Indexer
|
|||
$packageKey = null;
|
||||
$cacheKey = 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
|
||||
// Dynamic constraints are not cached, because they can change every time
|
||||
$packageVersion = ltrim($package->version, 'v');
|
||||
|
|
|
@ -8,23 +8,19 @@ use LanguageServer\Protocol\{
|
|||
ClientCapabilities,
|
||||
TextDocumentSyncKind,
|
||||
Message,
|
||||
MessageType,
|
||||
InitializeResult,
|
||||
SymbolInformation,
|
||||
TextDocumentIdentifier,
|
||||
CompletionOptions
|
||||
CompletionOptions,
|
||||
SignatureHelpOptions
|
||||
};
|
||||
use LanguageServer\FilesFinder\{FilesFinder, ClientFilesFinder, FileSystemFilesFinder};
|
||||
use LanguageServer\ContentRetriever\{ContentRetriever, ClientContentRetriever, FileSystemContentRetriever};
|
||||
use LanguageServer\Index\{DependenciesIndex, GlobalIndex, Index, ProjectIndex, StubsIndex};
|
||||
use LanguageServer\Cache\{FileSystemCache, ClientCache};
|
||||
use AdvancedJsonRpc;
|
||||
use Sabre\Event\{Loop, Promise};
|
||||
use Sabre\Event\Promise;
|
||||
use function Sabre\Event\coroutine;
|
||||
use Exception;
|
||||
use Throwable;
|
||||
use Webmozart\PathUtil\Path;
|
||||
use Sabre\Uri;
|
||||
|
||||
class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
||||
{
|
||||
|
@ -111,7 +107,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
|||
protected $definitionResolver;
|
||||
|
||||
/**
|
||||
* @param PotocolReader $reader
|
||||
* @param ProtocolReader $reader
|
||||
* @param 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
|
||||
$error = $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(
|
||||
(string)$e,
|
||||
AdvancedJsonRpc\ErrorCode::INTERNAL_ERROR,
|
||||
|
@ -275,8 +271,6 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
|||
$serverCapabilities->documentSymbolProvider = true;
|
||||
// Support "Find all symbols in workspace"
|
||||
$serverCapabilities->workspaceSymbolProvider = true;
|
||||
// Support "Format Code"
|
||||
$serverCapabilities->documentFormattingProvider = true;
|
||||
// Support "Go to definition"
|
||||
$serverCapabilities->definitionProvider = true;
|
||||
// Support "Find all references"
|
||||
|
@ -287,6 +281,10 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
|||
$serverCapabilities->completionProvider = new CompletionOptions;
|
||||
$serverCapabilities->completionProvider->resolveProvider = false;
|
||||
$serverCapabilities->completionProvider->triggerCharacters = ['$', '>'];
|
||||
|
||||
$serverCapabilities->signatureHelpProvider = new SignatureHelpOptions();
|
||||
$serverCapabilities->signatureHelpProvider->triggerCharacters = ['(', ','];
|
||||
|
||||
// Support global references
|
||||
$serverCapabilities->xworkspaceReferencesProvider = true;
|
||||
$serverCapabilities->xdefinitionProvider = true;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 = '';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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]);
|
||||
}
|
|
@ -3,27 +3,20 @@ declare(strict_types = 1);
|
|||
|
||||
namespace LanguageServer;
|
||||
|
||||
use LanguageServer\Protocol\{Diagnostic, DiagnosticSeverity, Range, Position, TextEdit};
|
||||
use LanguageServer\NodeVisitor\{
|
||||
NodeAtPositionFinder,
|
||||
ReferencesAdder,
|
||||
DocBlockParser,
|
||||
DefinitionCollector,
|
||||
ColumnCalculator,
|
||||
ReferencesCollector
|
||||
};
|
||||
use LanguageServer\Index\Index;
|
||||
use PhpParser\{Error, ErrorHandler, Node, NodeTraverser};
|
||||
use PhpParser\NodeVisitor\NameResolver;
|
||||
use LanguageServer\Protocol\{
|
||||
Diagnostic, Position, Range
|
||||
};
|
||||
use Microsoft\PhpParser;
|
||||
use Microsoft\PhpParser\Node;
|
||||
use phpDocumentor\Reflection\DocBlockFactory;
|
||||
use Sabre\Uri;
|
||||
|
||||
class PhpDocument
|
||||
{
|
||||
/**
|
||||
* The PHPParser instance
|
||||
*
|
||||
* @var Parser
|
||||
* @var PhpParser\Parser
|
||||
*/
|
||||
private $parser;
|
||||
|
||||
|
@ -53,19 +46,12 @@ class PhpDocument
|
|||
*/
|
||||
private $uri;
|
||||
|
||||
/**
|
||||
* The content of the document
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $content;
|
||||
|
||||
/**
|
||||
* The AST of the document
|
||||
*
|
||||
* @var Node[]
|
||||
* @var Node\SourceFileNode
|
||||
*/
|
||||
private $stmts;
|
||||
private $sourceFileNode;
|
||||
|
||||
/**
|
||||
* Map from fully qualified name (FQN) to Definition
|
||||
|
@ -77,7 +63,7 @@ class PhpDocument
|
|||
/**
|
||||
* Map from fully qualified name (FQN) to Node
|
||||
*
|
||||
* @var Node[]
|
||||
* @var Node
|
||||
*/
|
||||
private $definitionNodes;
|
||||
|
||||
|
@ -99,7 +85,7 @@ class PhpDocument
|
|||
* @param string $uri The URI of the document
|
||||
* @param string $content The content of the document
|
||||
* @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 DefinitionResolver $definitionResolver The DefinitionResolver to resolve definitions to symbols in the workspace
|
||||
*/
|
||||
|
@ -107,7 +93,7 @@ class PhpDocument
|
|||
string $uri,
|
||||
string $content,
|
||||
Index $index,
|
||||
Parser $parser,
|
||||
$parser,
|
||||
DocBlockFactory $docBlockFactory,
|
||||
DefinitionResolver $definitionResolver
|
||||
) {
|
||||
|
@ -133,15 +119,13 @@ class PhpDocument
|
|||
/**
|
||||
* Updates the content on this document.
|
||||
* 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
|
||||
* @return void
|
||||
*/
|
||||
public function updateContent(string $content)
|
||||
{
|
||||
$this->content = $content;
|
||||
|
||||
// Unregister old definitions
|
||||
if (isset($this->definitions)) {
|
||||
foreach ($this->definitions as $fqn => $definition) {
|
||||
|
@ -160,77 +144,28 @@ class PhpDocument
|
|||
$this->definitions = null;
|
||||
$this->definitionNodes = null;
|
||||
|
||||
$errorHandler = new ErrorHandler\Collecting;
|
||||
$stmts = $this->parser->parse($content, $errorHandler);
|
||||
$treeAnalyzer = new TreeAnalyzer($this->parser, $content, $this->docBlockFactory, $this->definitionResolver, $this->uri);
|
||||
|
||||
$this->diagnostics = [];
|
||||
foreach ($errorHandler->getErrors() as $error) {
|
||||
$this->diagnostics[] = Diagnostic::fromError($error, $this->content, DiagnosticSeverity::ERROR, 'php');
|
||||
}
|
||||
$this->diagnostics = $treeAnalyzer->getDiagnostics();
|
||||
|
||||
// $stmts can be null in case of a fatal parsing error
|
||||
if ($stmts) {
|
||||
$traverser = new NodeTraverser;
|
||||
$this->definitions = $treeAnalyzer->getDefinitions();
|
||||
|
||||
// Resolve aliased names to FQNs
|
||||
$traverser->addVisitor(new NameResolver($errorHandler));
|
||||
$this->definitionNodes = $treeAnalyzer->getDefinitionNodes();
|
||||
|
||||
// Add parentNode, previousSibling, nextSibling attributes
|
||||
$traverser->addVisitor(new ReferencesAdder($this));
|
||||
$this->referenceNodes = $treeAnalyzer->getReferenceNodes();
|
||||
|
||||
// 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) {
|
||||
foreach ($this->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);
|
||||
foreach ($this->referenceNodes as $fqn => $nodes) {
|
||||
// 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.
|
||||
$this->index->addReferenceUri((string)$fqn, $this->uri);
|
||||
}
|
||||
|
||||
$this->stmts = $stmts;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
$this->sourceFileNode = $treeAnalyzer->getSourceFileNode();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -240,7 +175,7 @@ class PhpDocument
|
|||
*/
|
||||
public function getContent()
|
||||
{
|
||||
return $this->content;
|
||||
return $this->sourceFileNode->fileContents;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -266,11 +201,11 @@ class PhpDocument
|
|||
/**
|
||||
* 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)
|
||||
{
|
||||
if ($this->stmts === null) {
|
||||
if ($this->sourceFileNode === null) {
|
||||
return null;
|
||||
}
|
||||
$traverser = new NodeTraverser;
|
||||
$finder = new NodeAtPositionFinder($position);
|
||||
$traverser->addVisitor($finder);
|
||||
$traverser->traverse($this->stmts);
|
||||
return $finder->node;
|
||||
|
||||
$offset = $position->toOffset($this->sourceFileNode->getFileContents());
|
||||
$node = $this->sourceFileNode->getDescendantNodeAtPosition($offset);
|
||||
if ($node !== null && $node->getStart() > $offset) {
|
||||
return null;
|
||||
}
|
||||
return $node;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -299,12 +236,10 @@ class PhpDocument
|
|||
*/
|
||||
public function getRange(Range $range)
|
||||
{
|
||||
if ($this->content === null) {
|
||||
return null;
|
||||
}
|
||||
$start = $range->start->toOffset($this->content);
|
||||
$length = $range->end->toOffset($this->content) - $start;
|
||||
return substr($this->content, $start, $length);
|
||||
$content = $this->getContent();
|
||||
$start = $range->start->toOffset($content);
|
||||
$length = $range->end->toOffset($content) - $start;
|
||||
return substr($content, $start, $length);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -8,6 +8,7 @@ use LanguageServer\Index\ProjectIndex;
|
|||
use phpDocumentor\Reflection\DocBlockFactory;
|
||||
use Sabre\Event\Promise;
|
||||
use function Sabre\Event\coroutine;
|
||||
use Microsoft\PhpParser;
|
||||
|
||||
/**
|
||||
* Takes care of loading documents and managing "open" documents
|
||||
|
@ -36,6 +37,11 @@ class PhpDocumentLoader
|
|||
*/
|
||||
private $parser;
|
||||
|
||||
/**
|
||||
* @var PhpParser\Parser
|
||||
*/
|
||||
private $tolerantParser;
|
||||
|
||||
/**
|
||||
* @var DocBlockFactory
|
||||
*/
|
||||
|
@ -48,8 +54,9 @@ class PhpDocumentLoader
|
|||
|
||||
/**
|
||||
* @param ContentRetriever $contentRetriever
|
||||
* @param ProjectIndex $project
|
||||
* @param ProjectIndex $projectIndex
|
||||
* @param DefinitionResolver $definitionResolver
|
||||
* @internal param ProjectIndex $project
|
||||
*/
|
||||
public function __construct(
|
||||
ContentRetriever $contentRetriever,
|
||||
|
@ -59,7 +66,7 @@ class PhpDocumentLoader
|
|||
$this->contentRetriever = $contentRetriever;
|
||||
$this->projectIndex = $projectIndex;
|
||||
$this->definitionResolver = $definitionResolver;
|
||||
$this->parser = new Parser;
|
||||
$this->parser = new PhpParser\Parser();
|
||||
$this->docBlockFactory = DocBlockFactory::createInstance();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
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
|
||||
* resource.
|
||||
|
@ -47,26 +45,6 @@ class Diagnostic
|
|||
*/
|
||||
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 Range $range The range at which the message applies
|
||||
|
|
|
@ -7,7 +7,7 @@ namespace LanguageServer\Protocol;
|
|||
* special attention. Usually a document highlight is visualized by changing
|
||||
* the background color of its range.
|
||||
*/
|
||||
class DocumentHighlightKind
|
||||
class DocumentHighlight
|
||||
{
|
||||
/**
|
||||
* The range this highlight applies to.
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
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.
|
||||
|
@ -25,9 +26,13 @@ class Location
|
|||
* @param Node $node
|
||||
* @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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -23,4 +23,17 @@ class ParameterInformation
|
|||
* @var string|null
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
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.
|
||||
|
@ -31,26 +32,12 @@ class Range
|
|||
*/
|
||||
public static function fromNode(Node $node)
|
||||
{
|
||||
return new self(
|
||||
new Position($node->getAttribute('startLine') - 1, $node->getAttribute('startColumn') - 1),
|
||||
new Position($node->getAttribute('endLine') - 1, $node->getAttribute('endColumn'))
|
||||
);
|
||||
}
|
||||
$range = PhpParser\PositionUtilities::getRangeFromPosition($node->getStart(), $node->getWidth(), $node->getFileContents());
|
||||
|
||||
/**
|
||||
* Returns the range where an error occured
|
||||
*
|
||||
* @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));
|
||||
return new self(
|
||||
new Position($range->start->line, $range->start->character),
|
||||
new Position($range->end->line, $range->end->character)
|
||||
);
|
||||
}
|
||||
|
||||
public function __construct(Position $start = null, Position $end = null)
|
||||
|
|
|
@ -29,4 +29,18 @@ class SignatureHelp
|
|||
* @var int|null
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,4 +31,19 @@ class SignatureInformation
|
|||
* @var ParameterInformation[]|null
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,10 @@ declare(strict_types = 1);
|
|||
|
||||
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.
|
||||
|
@ -13,19 +16,17 @@ class SymbolDescriptor extends SymbolInformation
|
|||
public $fqsen;
|
||||
|
||||
/**
|
||||
* A package from the composer.lock file or the contents of the composer.json
|
||||
* Example: https://github.com/composer/composer/blob/master/composer.lock#L10
|
||||
* Available fields may differ
|
||||
* Identifies the Composer package the symbol is defined in (if any)
|
||||
*
|
||||
* @var object|null
|
||||
* @var PackageDescriptor|null
|
||||
*/
|
||||
public $package;
|
||||
|
||||
/**
|
||||
* @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->package = $package;
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
use PhpParser\Node;
|
||||
use Exception;
|
||||
use Microsoft\PhpParser;
|
||||
use Microsoft\PhpParser\Node;
|
||||
|
||||
/**
|
||||
* Represents information about programming constructs like variables, classes,
|
||||
|
@ -44,51 +44,70 @@ class SymbolInformation
|
|||
*
|
||||
* @param Node $node
|
||||
* @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;
|
||||
if ($node instanceof Node\Stmt\Class_) {
|
||||
if ($node instanceof Node\Statement\ClassDeclaration) {
|
||||
$symbol->kind = SymbolKind::CLASS_;
|
||||
} else if ($node instanceof Node\Stmt\Trait_) {
|
||||
} else if ($node instanceof Node\Statement\TraitDeclaration) {
|
||||
$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;
|
||||
} else if ($node instanceof Node\Name && $parent instanceof Node\Stmt\Namespace_) {
|
||||
} else if ($node instanceof Node\Statement\NamespaceDefinition) {
|
||||
$symbol->kind = SymbolKind::NAMESPACE;
|
||||
} else if ($node instanceof Node\Stmt\Function_) {
|
||||
} else if ($node instanceof Node\Statement\FunctionDeclaration) {
|
||||
$symbol->kind = SymbolKind::FUNCTION;
|
||||
} else if ($node instanceof Node\Stmt\ClassMethod) {
|
||||
} else if ($node instanceof Node\MethodDeclaration) {
|
||||
$nameText = $node->getName();
|
||||
if ($nameText === '__construct' || $nameText === '__destruct') {
|
||||
$symbol->kind = SymbolKind::CONSTRUCTOR;
|
||||
} else {
|
||||
$symbol->kind = SymbolKind::METHOD;
|
||||
} else if ($node instanceof Node\Stmt\PropertyProperty) {
|
||||
}
|
||||
} else if ($node instanceof Node\Expression\Variable && $node->getFirstAncestor(Node\PropertyDeclaration::class) !== null) {
|
||||
$symbol->kind = SymbolKind::PROPERTY;
|
||||
} else if ($node instanceof Node\Const_) {
|
||||
} else if ($node instanceof Node\ConstElement) {
|
||||
$symbol->kind = SymbolKind::CONSTANT;
|
||||
} else if (
|
||||
(
|
||||
($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignOp)
|
||||
&& $node->var instanceof Node\Expr\Variable
|
||||
($node instanceof Node\Expression\AssignmentExpression)
|
||||
&& $node->leftOperand instanceof Node\Expression\Variable
|
||||
)
|
||||
|| $node instanceof Node\Expr\ClosureUse
|
||||
|| $node instanceof Node\Param
|
||||
|| $node instanceof Node\UseVariableName
|
||||
|| $node instanceof Node\Parameter
|
||||
) {
|
||||
$symbol->kind = SymbolKind::VARIABLE;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
if ($node instanceof Node\Name) {
|
||||
$symbol->name = (string)$node;
|
||||
} else if ($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignOp) {
|
||||
$symbol->name = $node->var->name;
|
||||
} else if ($node instanceof Node\Expr\ClosureUse) {
|
||||
$symbol->name = $node->var;
|
||||
|
||||
if ($node instanceof Node\Expression\AssignmentExpression) {
|
||||
if ($node->leftOperand instanceof Node\Expression\Variable) {
|
||||
$symbol->name = $node->leftOperand->getName();
|
||||
} elseif ($node->leftOperand instanceof PhpParser\Token) {
|
||||
$symbol->name = trim($node->leftOperand->getText($node->getFileContents()), "$");
|
||||
}
|
||||
} else if ($node instanceof Node\UseVariableName) {
|
||||
$symbol->name = $node->getName();
|
||||
} else if (isset($node->name)) {
|
||||
$symbol->name = (string)$node->name;
|
||||
if ($node->name instanceof Node\QualifiedName) {
|
||||
$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;
|
||||
}
|
||||
|
||||
$symbol->location = Location::fromNode($node);
|
||||
if ($fqn !== null) {
|
||||
$parts = preg_split('/(::|->|\\\\)/', $fqn);
|
||||
|
|
|
@ -8,7 +8,6 @@ use Sabre\Event\{
|
|||
Loop,
|
||||
Promise
|
||||
};
|
||||
use RuntimeException;
|
||||
|
||||
class ProtocolStreamWriter implements ProtocolWriter
|
||||
{
|
||||
|
|
|
@ -3,34 +3,33 @@ declare(strict_types = 1);
|
|||
|
||||
namespace LanguageServer\Server;
|
||||
|
||||
use PhpParser\PrettyPrinter\Standard as PrettyPrinter;
|
||||
use PhpParser\{Node, NodeTraverser};
|
||||
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\{
|
||||
CompletionProvider, SignatureHelpProvider, LanguageClient, PhpDocument, PhpDocumentLoader, DefinitionResolver
|
||||
};
|
||||
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\Uri;
|
||||
use function LanguageServer\{
|
||||
isVendored, waitForEvent, getPackageName
|
||||
};
|
||||
use function Sabre\Event\coroutine;
|
||||
use function LanguageServer\{waitForEvent, isVendored};
|
||||
|
||||
/**
|
||||
* Provides method handlers for all textDocument/* methods
|
||||
|
@ -49,11 +48,6 @@ class TextDocument
|
|||
*/
|
||||
protected $project;
|
||||
|
||||
/**
|
||||
* @var PrettyPrinter
|
||||
*/
|
||||
protected $prettyPrinter;
|
||||
|
||||
/**
|
||||
* @var DefinitionResolver
|
||||
*/
|
||||
|
@ -64,6 +58,11 @@ class TextDocument
|
|||
*/
|
||||
protected $completionProvider;
|
||||
|
||||
/**
|
||||
* @var SignatureHelpProvider
|
||||
*/
|
||||
protected $signatureHelpProvider;
|
||||
|
||||
/**
|
||||
* @var ReadableIndex
|
||||
*/
|
||||
|
@ -97,9 +96,9 @@ class TextDocument
|
|||
) {
|
||||
$this->documentLoader = $documentLoader;
|
||||
$this->client = $client;
|
||||
$this->prettyPrinter = new PrettyPrinter();
|
||||
$this->definitionResolver = $definitionResolver;
|
||||
$this->completionProvider = new CompletionProvider($this->definitionResolver, $index);
|
||||
$this->signatureHelpProvider = new SignatureHelpProvider($this->definitionResolver, $index, $documentLoader);
|
||||
$this->index = $index;
|
||||
$this->composerJson = $composerJson;
|
||||
$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
|
||||
* 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
|
||||
*/
|
||||
public function didClose(TextDocumentIdentifier $textDocument)
|
||||
|
@ -166,20 +165,6 @@ class TextDocument
|
|||
$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
|
||||
* 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
|
||||
// by traversing the AST
|
||||
if (
|
||||
$node instanceof Node\Expr\Variable
|
||||
|| $node instanceof Node\Param
|
||||
|| $node instanceof Node\Expr\ClosureUse
|
||||
|
||||
($node instanceof Node\Expression\Variable && !($node->getParent()->getParent() instanceof Node\PropertyDeclaration))
|
||||
|| $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;
|
||||
}
|
||||
// Find function/method/closure scope
|
||||
$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 &&
|
||||
$descendantNode->getName() === $node->getName()
|
||||
) {
|
||||
$locations[] = Location::fromNode($descendantNode);
|
||||
}
|
||||
$traverser = new NodeTraverser;
|
||||
$refCollector = new VariableReferencesCollector($node->name);
|
||||
$traverser->addVisitor($refCollector);
|
||||
$traverser->traverse($n->getStmts());
|
||||
foreach ($refCollector->nodes as $ref) {
|
||||
$locations[] = Location::fromNode($ref);
|
||||
}
|
||||
} else {
|
||||
// Definition with a global FQN
|
||||
$fqn = DefinitionResolver::getDefinedFqn($node);
|
||||
|
||||
// Wait until indexing finished
|
||||
if (!$this->index->isComplete()) {
|
||||
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
|
||||
* at a given text document position.
|
||||
|
@ -331,6 +336,7 @@ class TextDocument
|
|||
if ($def === null) {
|
||||
return new Hover([], $range);
|
||||
}
|
||||
$contents = [];
|
||||
if ($def->declarationLine) {
|
||||
$contents[] = new MarkedString('php', "<?php\n" . $def->declarationLine);
|
||||
}
|
||||
|
@ -353,13 +359,14 @@ class TextDocument
|
|||
*
|
||||
* @param TextDocumentIdentifier The text document
|
||||
* @param Position $position The position
|
||||
* @param CompletionContext|null $context The completion context
|
||||
* @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);
|
||||
return $this->completionProvider->provideCompletion($document, $position);
|
||||
return $this->completionProvider->provideCompletion($document, $position, $context);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -384,9 +391,10 @@ class TextDocument
|
|||
return [];
|
||||
}
|
||||
// Handle definition nodes
|
||||
$fqn = DefinitionResolver::getDefinedFqn($node);
|
||||
while (true) {
|
||||
if ($fqn) {
|
||||
$def = $this->index->getDefinition($definedFqn);
|
||||
$def = $this->index->getDefinition($fqn);
|
||||
} else {
|
||||
// Handle reference nodes
|
||||
$def = $this->definitionResolver->resolveReferenceNodeToDefinition($node);
|
||||
|
@ -404,25 +412,14 @@ class TextDocument
|
|||
) {
|
||||
return [];
|
||||
}
|
||||
$symbol = new SymbolDescriptor;
|
||||
foreach (get_object_vars($def->symbolInformation) as $prop => $val) {
|
||||
$symbol->$prop = $val;
|
||||
}
|
||||
$symbol->fqsen = $def->fqn;
|
||||
// if Definition is inside a dependency, use the package name
|
||||
$packageName = getPackageName($def->symbolInformation->location->uri, $this->composerJson);
|
||||
if ($packageName && $this->composerLock !== null) {
|
||||
// Definition is inside a dependency
|
||||
foreach (array_merge($this->composerLock->packages, $this->composerLock->{'packages-dev'}) as $package) {
|
||||
if ($package->name === $packageName) {
|
||||
$symbol->package = $package;
|
||||
break;
|
||||
// else use the package name of the root package (if exists)
|
||||
if (!$packageName && $this->composerJson !== null) {
|
||||
$packageName = $this->composerJson->name;
|
||||
}
|
||||
}
|
||||
} 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)];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ declare(strict_types = 1);
|
|||
|
||||
namespace LanguageServer\Server;
|
||||
|
||||
use LanguageServer\{LanguageClient, Project, PhpDocumentLoader, Options, Indexer};
|
||||
use LanguageServer\{LanguageClient, PhpDocumentLoader};
|
||||
use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index};
|
||||
use LanguageServer\Protocol\{
|
||||
FileChangeType,
|
||||
|
@ -16,7 +16,7 @@ use LanguageServer\Protocol\{
|
|||
};
|
||||
use Sabre\Event\Promise;
|
||||
use function Sabre\Event\coroutine;
|
||||
use function LanguageServer\{waitForEvent, getPackageName};
|
||||
use function LanguageServer\waitForEvent;
|
||||
|
||||
/**
|
||||
* Provides method handlers for all workspace/* methods
|
||||
|
@ -33,7 +33,7 @@ class Workspace
|
|||
*
|
||||
* @var ProjectIndex
|
||||
*/
|
||||
private $index;
|
||||
private $projectIndex;
|
||||
|
||||
/**
|
||||
* @var DependenciesIndex
|
||||
|
@ -45,16 +45,6 @@ class Workspace
|
|||
*/
|
||||
private $sourceIndex;
|
||||
|
||||
/**
|
||||
* @var Options
|
||||
*/
|
||||
private $options;
|
||||
|
||||
/**
|
||||
* @var Indexer
|
||||
*/
|
||||
private $indexer;
|
||||
|
||||
/**
|
||||
* @var \stdClass
|
||||
*/
|
||||
|
@ -67,25 +57,21 @@ class Workspace
|
|||
|
||||
/**
|
||||
* @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 $sourceIndex Index that is used on a workspace/xreferences request
|
||||
* @param \stdClass $composerLock The parsed composer.lock of the project, if any
|
||||
* @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->sourceIndex = $sourceIndex;
|
||||
$this->index = $index;
|
||||
$this->projectIndex = $projectIndex;
|
||||
$this->dependenciesIndex = $dependenciesIndex;
|
||||
$this->composerLock = $composerLock;
|
||||
$this->documentLoader = $documentLoader;
|
||||
$this->composerJson = $composerJson;
|
||||
$this->indexer = $indexer;
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -98,11 +84,11 @@ class Workspace
|
|||
{
|
||||
return coroutine(function () use ($query) {
|
||||
// Wait until indexing for definitions finished
|
||||
if (!$this->index->isStaticComplete()) {
|
||||
yield waitForEvent($this->index, 'static-complete');
|
||||
if (!$this->sourceIndex->isStaticComplete()) {
|
||||
yield waitForEvent($this->sourceIndex, 'static-complete');
|
||||
}
|
||||
$symbols = [];
|
||||
foreach ($this->index->getDefinitions() as $fqn => $definition) {
|
||||
foreach ($this->sourceIndex->getDefinitions() as $fqn => $definition) {
|
||||
if ($query === '' || stripos($fqn, $query) !== false) {
|
||||
$symbols[] = $definition->symbolInformation;
|
||||
}
|
||||
|
@ -135,13 +121,14 @@ class Workspace
|
|||
*/
|
||||
public function xreferences($query, array $files = null): Promise
|
||||
{
|
||||
// TODO: $files is unused in the coroutine
|
||||
return coroutine(function () use ($query, $files) {
|
||||
if ($this->composerLock === null) {
|
||||
return [];
|
||||
}
|
||||
// Wait until indexing finished
|
||||
if (!$this->index->isComplete()) {
|
||||
yield waitForEvent($this->index, 'complete');
|
||||
if (!$this->projectIndex->isComplete()) {
|
||||
yield waitForEvent($this->projectIndex, 'complete');
|
||||
}
|
||||
/** Map from URI to array of referenced FQNs in dependencies */
|
||||
$refs = [];
|
||||
|
@ -160,38 +147,11 @@ class Workspace
|
|||
$refInfos = [];
|
||||
foreach ($refs as $uri => $fqns) {
|
||||
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);
|
||||
foreach ($doc->getReferenceNodesByFqn($fqn) as $node) {
|
||||
$refInfo = new ReferenceInformation;
|
||||
$refInfo->reference = Location::fromNode($node);
|
||||
$refInfo->symbol = $symbol;
|
||||
$refInfo->symbol = $query;
|
||||
$refInfos[] = $refInfo;
|
||||
}
|
||||
}
|
||||
|
@ -209,80 +169,9 @@ class Workspace
|
|||
return [];
|
||||
}
|
||||
$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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -5,7 +5,6 @@ namespace LanguageServer;
|
|||
|
||||
use Throwable;
|
||||
use InvalidArgumentException;
|
||||
use PhpParser\Node;
|
||||
use Sabre\Event\{Loop, Promise, EmitterInterface};
|
||||
use Sabre\Uri;
|
||||
|
||||
|
@ -94,23 +93,6 @@ function waitForEvent(EmitterInterface $emitter, string $event): Promise
|
|||
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
|
||||
* 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
|
||||
* is a vendored URI
|
||||
*
|
||||
* @param \stdClass|null $composerJson
|
||||
* @param string $uri
|
||||
* @param array $matches
|
||||
* @param \stdClass|null $composerJson
|
||||
* @return string|null
|
||||
*/
|
||||
function getPackageName(string $uri, \stdClass $composerJson = null)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -15,7 +15,8 @@ use LanguageServer\Protocol\{
|
|||
TextDocumentIdentifier,
|
||||
InitializeResult,
|
||||
ServerCapabilities,
|
||||
CompletionOptions
|
||||
CompletionOptions,
|
||||
SignatureHelpOptions
|
||||
};
|
||||
use AdvancedJsonRpc;
|
||||
use Webmozart\Glob\Glob;
|
||||
|
@ -35,13 +36,14 @@ class LanguageServerTest extends TestCase
|
|||
$serverCapabilities->textDocumentSync = TextDocumentSyncKind::FULL;
|
||||
$serverCapabilities->documentSymbolProvider = true;
|
||||
$serverCapabilities->workspaceSymbolProvider = true;
|
||||
$serverCapabilities->documentFormattingProvider = true;
|
||||
$serverCapabilities->definitionProvider = true;
|
||||
$serverCapabilities->referencesProvider = true;
|
||||
$serverCapabilities->hoverProvider = true;
|
||||
$serverCapabilities->completionProvider = new CompletionOptions;
|
||||
$serverCapabilities->completionProvider->resolveProvider = false;
|
||||
$serverCapabilities->completionProvider->triggerCharacters = ['$', '>'];
|
||||
$serverCapabilities->signatureHelpProvider = new SignatureHelpOptions;
|
||||
$serverCapabilities->signatureHelpProvider->triggerCharacters = ['(', ','];
|
||||
$serverCapabilities->xworkspaceReferencesProvider = true;
|
||||
$serverCapabilities->xdefinitionProvider = 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->params->type === MessageType::ERROR) {
|
||||
$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();
|
||||
}
|
||||
}
|
||||
|
@ -104,7 +106,7 @@ class LanguageServerTest extends TestCase
|
|||
if ($promise->state === Promise::PENDING) {
|
||||
$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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
}
|
|
@ -4,39 +4,21 @@ declare(strict_types = 1);
|
|||
namespace LanguageServer\Tests\Server\TextDocument;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use PhpParser\{NodeTraverser, Node};
|
||||
use PhpParser\NodeVisitor\NameResolver;
|
||||
use phpDocumentor\Reflection\DocBlockFactory;
|
||||
use LanguageServer\{LanguageClient, PhpDocument, PhpDocumentLoader, Parser, DefinitionResolver};
|
||||
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
|
||||
use LanguageServer\Protocol\ClientCapabilities;
|
||||
use LanguageServer\Index\{ProjectIndex, Index, DependenciesIndex};
|
||||
use LanguageServer\Tests\MockProtocolStream;
|
||||
use LanguageServer\NodeVisitor\{ReferencesAdder, DefinitionCollector};
|
||||
use LanguageServer\{
|
||||
DefinitionResolver, TreeAnalyzer
|
||||
};
|
||||
use LanguageServer\Index\{Index};
|
||||
use function LanguageServer\pathToUri;
|
||||
use Microsoft\PhpParser;
|
||||
use Microsoft\PhpParser\Node;
|
||||
|
||||
class DefinitionCollectorTest extends TestCase
|
||||
{
|
||||
public function testCollectsSymbols()
|
||||
{
|
||||
$path = realpath(__DIR__ . '/../../fixtures/symbols.php');
|
||||
$uri = pathToUri($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;
|
||||
$defNodes = $this->collectDefinitions($path);
|
||||
|
||||
$this->assertEquals([
|
||||
'TestNamespace',
|
||||
|
@ -50,45 +32,53 @@ class DefinitionCollectorTest extends TestCase
|
|||
'TestNamespace\\TestTrait',
|
||||
'TestNamespace\\TestInterface',
|
||||
'TestNamespace\\test_function()',
|
||||
'TestNamespace\\ChildClass'
|
||||
'TestNamespace\\ChildClass',
|
||||
'TestNamespace\\Example',
|
||||
'TestNamespace\\Example->__construct()',
|
||||
'TestNamespace\\Example->__destruct()'
|
||||
], array_keys($defNodes));
|
||||
$this->assertInstanceOf(Node\Const_::class, $defNodes['TestNamespace\\TEST_CONST']);
|
||||
$this->assertInstanceOf(Node\Stmt\Class_::class, $defNodes['TestNamespace\\TestClass']);
|
||||
$this->assertInstanceOf(Node\Const_::class, $defNodes['TestNamespace\\TestClass::TEST_CLASS_CONST']);
|
||||
$this->assertInstanceOf(Node\Stmt\PropertyProperty::class, $defNodes['TestNamespace\\TestClass::$staticTestProperty']);
|
||||
$this->assertInstanceOf(Node\Stmt\PropertyProperty::class, $defNodes['TestNamespace\\TestClass->testProperty']);
|
||||
$this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defNodes['TestNamespace\\TestClass::staticTestMethod()']);
|
||||
$this->assertInstanceOf(Node\Stmt\ClassMethod::class, $defNodes['TestNamespace\\TestClass->testMethod()']);
|
||||
$this->assertInstanceOf(Node\Stmt\Trait_::class, $defNodes['TestNamespace\\TestTrait']);
|
||||
$this->assertInstanceOf(Node\Stmt\Interface_::class, $defNodes['TestNamespace\\TestInterface']);
|
||||
$this->assertInstanceOf(Node\Stmt\Function_::class, $defNodes['TestNamespace\\test_function()']);
|
||||
$this->assertInstanceOf(Node\Stmt\Class_::class, $defNodes['TestNamespace\\ChildClass']);
|
||||
|
||||
$this->assertInstanceOf(Node\ConstElement::class, $defNodes['TestNamespace\\TEST_CONST']);
|
||||
$this->assertInstanceOf(Node\Statement\ClassDeclaration::class, $defNodes['TestNamespace\\TestClass']);
|
||||
$this->assertInstanceOf(Node\ConstElement::class, $defNodes['TestNamespace\\TestClass::TEST_CLASS_CONST']);
|
||||
// TODO - should we parse properties more strictly?
|
||||
$this->assertInstanceOf(Node\Expression\Variable::class, $defNodes['TestNamespace\\TestClass::$staticTestProperty']);
|
||||
$this->assertInstanceOf(Node\Expression\Variable::class, $defNodes['TestNamespace\\TestClass->testProperty']);
|
||||
$this->assertInstanceOf(Node\MethodDeclaration::class, $defNodes['TestNamespace\\TestClass::staticTestMethod()']);
|
||||
$this->assertInstanceOf(Node\MethodDeclaration::class, $defNodes['TestNamespace\\TestClass->testMethod()']);
|
||||
$this->assertInstanceOf(Node\Statement\TraitDeclaration::class, $defNodes['TestNamespace\\TestTrait']);
|
||||
$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()
|
||||
{
|
||||
$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);
|
||||
$parser = new Parser;
|
||||
$parser = new PhpParser\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(['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()']);
|
||||
$treeAnalyzer = new TreeAnalyzer($parser, $content, $docBlockFactory, $definitionResolver, $uri);
|
||||
return $treeAnalyzer->getDefinitionNodes();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,20 +3,14 @@ declare(strict_types = 1);
|
|||
|
||||
namespace LanguageServer\Tests\Server;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use LanguageServer\Tests\MockProtocolStream;
|
||||
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 LanguageServer\{
|
||||
PhpDocument, PhpDocumentLoader, Project, DefinitionResolver
|
||||
};
|
||||
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;
|
||||
|
||||
class PhpDocumentLoaderTest extends TestCase
|
||||
|
|
|
@ -3,22 +3,26 @@ declare(strict_types = 1);
|
|||
|
||||
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 LanguageServer\Tests\MockProtocolStream;
|
||||
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 PHPUnit\Framework\TestCase;
|
||||
use function LanguageServer\isVendored;
|
||||
|
||||
class PhpDocumentTest extends TestCase
|
||||
{
|
||||
public function createDocument(string $uri, string $content)
|
||||
{
|
||||
$parser = new Parser;
|
||||
$parser = new PhpParser\Parser();
|
||||
$docBlockFactory = DocBlockFactory::createInstance();
|
||||
$index = new Index;
|
||||
$definitionResolver = new DefinitionResolver($index);
|
||||
|
@ -36,10 +40,15 @@ class PhpDocumentTest extends TestCase
|
|||
{
|
||||
$document = $this->createDocument('whatever', "<?php\n$\$a = new SomeClass;");
|
||||
$node = $document->getNodeAtPosition(new Position(1, 13));
|
||||
$this->assertInstanceOf(Node\Name\FullyQualified::class, $node);
|
||||
$this->assertQualifiedName($node);
|
||||
$this->assertEquals('SomeClass', (string)$node);
|
||||
}
|
||||
|
||||
private function assertQualifiedName($node)
|
||||
{
|
||||
$this->assertInstanceOf(Node\QualifiedName::class, $node);
|
||||
}
|
||||
|
||||
public function testIsVendored()
|
||||
{
|
||||
$document = $this->createDocument('file:///dir/vendor/x.php', "<?php\n$\$a = new SomeClass;");
|
||||
|
|
|
@ -5,14 +5,13 @@ namespace LanguageServer\Tests\Server;
|
|||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use LanguageServer\Tests\MockProtocolStream;
|
||||
use LanguageServer\{Server, LanguageClient, PhpDocumentLoader, DefinitionResolver, Options, Indexer};
|
||||
use LanguageServer\Index\{ProjectIndex, StubsIndex, GlobalIndex, DependenciesIndex, Index};
|
||||
use LanguageServer\{
|
||||
Server, LanguageClient, PhpDocumentLoader, DefinitionResolver
|
||||
};
|
||||
use LanguageServer\Index\{ProjectIndex, DependenciesIndex, Index};
|
||||
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
|
||||
use LanguageServer\Protocol\{Position, Location, Range, ClientCapabilities};
|
||||
use LanguageServer\FilesFinder\FileSystemFilesFinder;
|
||||
use LanguageServer\Cache\FileSystemCache;
|
||||
use LanguageServer\Protocol\{Position, Location, Range};
|
||||
use function LanguageServer\pathToUri;
|
||||
use Sabre\Event\Promise;
|
||||
|
||||
abstract class ServerTestCase extends TestCase
|
||||
{
|
||||
|
@ -52,17 +51,11 @@ abstract class ServerTestCase extends TestCase
|
|||
$projectIndex = new ProjectIndex($sourceIndex, $dependenciesIndex);
|
||||
$projectIndex->setComplete();
|
||||
|
||||
$rootPath = realpath(__DIR__ . '/../../fixtures/');
|
||||
$options = new Options;
|
||||
$filesFinder = new FileSystemFilesFinder;
|
||||
$cache = new FileSystemCache;
|
||||
|
||||
$definitionResolver = new DefinitionResolver($projectIndex);
|
||||
$client = new LanguageClient(new MockProtocolStream, new MockProtocolStream);
|
||||
$this->documentLoader = new PhpDocumentLoader(new FileSystemContentRetriever, $projectIndex, $definitionResolver);
|
||||
$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, null, $indexer, $options);
|
||||
$this->workspace = new Server\Workspace($client, $projectIndex, $dependenciesIndex, $sourceIndex, null, $this->documentLoader);
|
||||
|
||||
$globalSymbolsUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_symbols.php'));
|
||||
$globalReferencesUri = pathToUri(realpath(__DIR__ . '/../../fixtures/global_references.php'));
|
||||
|
@ -80,6 +73,7 @@ abstract class ServerTestCase extends TestCase
|
|||
$this->definitionLocations = [
|
||||
|
||||
// Global
|
||||
'TEST_DEFINE_CONSTANT' => new Location($globalSymbolsUri, new Range(new Position(104, 0), new Position(104, 37))),
|
||||
'TEST_CONST' => new Location($globalSymbolsUri, new Range(new Position( 9, 6), new Position( 9, 22))),
|
||||
'TestClass' => new Location($globalSymbolsUri, new Range(new Position(20, 0), new Position(61, 1))),
|
||||
'ChildClass' => new Location($globalSymbolsUri, new Range(new Position(99, 0), new Position(99, 37))),
|
||||
|
@ -91,11 +85,14 @@ abstract class ServerTestCase extends TestCase
|
|||
'TestClass::staticTestMethod()' => new Location($globalSymbolsUri, new Range(new Position(46, 4), new Position(49, 5))),
|
||||
'TestClass::testMethod()' => new Location($globalSymbolsUri, new Range(new Position(57, 4), new Position(60, 5))),
|
||||
'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
|
||||
'TestNamespace' => new Location($symbolsUri, new Range(new Position( 2, 10), new Position( 2, 23))),
|
||||
'SecondTestNamespace' => new Location($useUri, new Range(new Position( 2, 10), new Position( 2, 29))),
|
||||
'TestNamespace' => new Location($symbolsUri, new Range(new Position( 2, 0), new Position( 2, 24))),
|
||||
'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\\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))),
|
||||
|
@ -107,7 +104,10 @@ abstract class ServerTestCase extends TestCase
|
|||
'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\\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 = [
|
||||
|
@ -116,21 +116,22 @@ abstract class ServerTestCase extends TestCase
|
|||
'TestNamespace' => [
|
||||
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;
|
||||
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' => [
|
||||
0 => new Location($referencesUri, new Range(new Position(29, 5), new Position(29, 15)))
|
||||
],
|
||||
'TestNamespace\\TestClass' => [
|
||||
0 => new Location($symbolsUri , new Range(new Position(99, 25), new Position(99, 34))), // class ChildClass extends TestClass {}
|
||||
1 => new Location($referencesUri, new Range(new Position( 4, 11), new Position( 4, 20))), // $obj = new TestClass();
|
||||
2 => new Location($referencesUri, new Range(new Position( 7, 0), new Position( 7, 9))), // TestClass::staticTestMethod();
|
||||
3 => new Location($referencesUri, new Range(new Position( 8, 5), new Position( 8, 14))), // echo TestClass::$staticTestProperty;
|
||||
4 => new Location($referencesUri, new Range(new Position( 9, 5), new Position( 9, 14))), // TestClass::TEST_CLASS_CONST;
|
||||
5 => new Location($referencesUri, new Range(new Position(21, 18), new Position(21, 27))), // function whatever(TestClass $param)
|
||||
6 => new Location($referencesUri, new Range(new Position(21, 37), new Position(21, 46))), // function whatever(TestClass $param): TestClass
|
||||
7 => new Location($referencesUri, new Range(new Position(39, 0), new Position(39, 9))), // TestClass::$staticTestProperty[123]->testProperty;
|
||||
8 => new Location($useUri, new Range(new Position( 4, 4), new Position( 4, 27))), // use TestNamespace\TestClass;
|
||||
0 => new Location($symbolsUri, new Range(new Position(48, 13), new Position(48, 17))), // echo self::TEST_CLASS_CONST;
|
||||
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( 4, 11), new Position( 4, 20))), // $obj = new TestClass();
|
||||
3 => new Location($referencesUri, new Range(new Position( 7, 0), new Position( 7, 9))), // TestClass::staticTestMethod();
|
||||
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( 9, 5), new Position( 9, 14))), // TestClass::TEST_CLASS_CONST;
|
||||
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(21, 37), new Position(21, 46))), // function whatever(TestClass $param): 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' => [
|
||||
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;
|
||||
],
|
||||
'TestNamespace\\TestClass::staticTestProperty' => [
|
||||
0 => new Location($referencesUri, new Range(new Position( 8, 5), 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;
|
||||
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, 11), new Position(39, 30))) // TestClass::$staticTestProperty[123]->testProperty;
|
||||
],
|
||||
'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()' => [
|
||||
0 => new Location($referencesUri, new Range(new Position( 5, 0), new Position( 5, 18))), // $obj->testMethod();
|
||||
1 => new Location($referencesUri, new Range(new Position(38, 0), new Position(38, 32))), // $obj->testProperty->testMethod();
|
||||
2 => new Location($referencesUri, new Range(new Position(42, 5), new Position(42, 25))) // $child->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, 30))), // $obj->testProperty->testMethod();
|
||||
2 => new Location($referencesUri, new Range(new Position(42, 5), new Position(42, 23))) // $child->testMethod();
|
||||
],
|
||||
'TestNamespace\\test_function()' => [
|
||||
0 => new Location($referencesUri, new Range(new Position(10, 0), new Position(10, 13))),
|
||||
|
@ -168,26 +169,30 @@ abstract class ServerTestCase extends TestCase
|
|||
],
|
||||
|
||||
// Global
|
||||
'TEST_DEFINE_CONSTANT' => [
|
||||
0 => new Location($globalSymbolsUri, new Range(new Position(106, 6), new Position(106, 26)))
|
||||
],
|
||||
'TEST_CONST' => [
|
||||
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)))
|
||||
],
|
||||
'TestClass' => [
|
||||
0 => new Location($globalSymbolsUri, new Range(new Position(99, 25), new Position(99, 34))), // class ChildClass extends TestClass {}
|
||||
1 => new Location($globalReferencesUri, new Range(new Position( 4, 11), new Position( 4, 20))), // $obj = new TestClass();
|
||||
2 => new Location($globalReferencesUri, new Range(new Position( 7, 0), new Position( 7, 9))), // TestClass::staticTestMethod();
|
||||
3 => new Location($globalReferencesUri, new Range(new Position( 8, 5), new Position( 8, 14))), // echo TestClass::$staticTestProperty;
|
||||
4 => new Location($globalReferencesUri, new Range(new Position( 9, 5), new Position( 9, 14))), // TestClass::TEST_CLASS_CONST;
|
||||
5 => new Location($globalReferencesUri, new Range(new Position(21, 18), new Position(21, 27))), // function whatever(TestClass $param)
|
||||
6 => new Location($globalReferencesUri, new Range(new Position(21, 37), new Position(21, 46))), // function whatever(TestClass $param): TestClass
|
||||
7 => new Location($globalReferencesUri, new Range(new Position(39, 0), new Position(39, 9))), // TestClass::$staticTestProperty[123]->testProperty;
|
||||
0 => new Location($globalSymbolsUri, new Range(new Position(48, 13), new Position(48, 17))), // echo self::TEST_CLASS_CONST;
|
||||
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( 4, 11), new Position( 4, 20))), // $obj = new TestClass();
|
||||
3 => new Location($globalReferencesUri, new Range(new Position( 7, 0), new Position( 7, 9))), // TestClass::staticTestMethod();
|
||||
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( 9, 5), new Position( 9, 14))), // TestClass::TEST_CLASS_CONST;
|
||||
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(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' => [
|
||||
0 => new Location($globalReferencesUri, new Range(new Position(42, 5), new Position(42, 25))), // echo $child->testProperty;
|
||||
],
|
||||
'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)
|
||||
],
|
||||
'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;
|
||||
],
|
||||
'TestClass::staticTestProperty' => [
|
||||
0 => new Location($globalReferencesUri, new Range(new Position( 8, 5), 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;
|
||||
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, 11), new Position(39, 30))) // TestClass::$staticTestProperty[123]->testProperty;
|
||||
],
|
||||
'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()' => [
|
||||
0 => new Location($globalReferencesUri, new Range(new Position( 5, 0), new Position( 5, 18))), // $obj->testMethod();
|
||||
1 => new Location($globalReferencesUri, new Range(new Position(38, 0), new Position(38, 32))), // $obj->testProperty->testMethod();
|
||||
2 => new Location($globalReferencesUri, new Range(new Position(42, 5), new Position(42, 25))) // $child->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, 30))), // $obj->testProperty->testMethod();
|
||||
2 => new Location($globalReferencesUri, new Range(new Position(42, 5), new Position(42, 23))) // $child->testMethod();
|
||||
],
|
||||
'test_function()' => [
|
||||
0 => new Location($globalReferencesUri, new Range(new Position(10, 0), new Position(10, 13))),
|
||||
|
|
|
@ -5,18 +5,21 @@ namespace LanguageServer\Tests\Server\TextDocument;
|
|||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use LanguageServer\Tests\MockProtocolStream;
|
||||
use LanguageServer\{Server, LanguageClient, PhpDocumentLoader, CompletionProvider, DefinitionResolver};
|
||||
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex, GlobalIndex, StubsIndex};
|
||||
use LanguageServer\{
|
||||
Server, LanguageClient, PhpDocumentLoader, DefinitionResolver
|
||||
};
|
||||
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
|
||||
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
|
||||
use LanguageServer\Protocol\{
|
||||
TextDocumentIdentifier,
|
||||
TextEdit,
|
||||
Range,
|
||||
Position,
|
||||
ClientCapabilities,
|
||||
CompletionList,
|
||||
CompletionItem,
|
||||
CompletionItemKind
|
||||
CompletionItemKind,
|
||||
CompletionContext,
|
||||
CompletionTriggerKind
|
||||
};
|
||||
use function LanguageServer\pathToUri;
|
||||
|
||||
|
@ -52,7 +55,7 @@ class CompletionTest extends TestCase
|
|||
new TextDocumentIdentifier($completionUri),
|
||||
new Position(3, 7)
|
||||
)->wait();
|
||||
$this->assertEquals(new CompletionList([
|
||||
$this->assertCompletionsListSubset(new CompletionList([
|
||||
new CompletionItem(
|
||||
'testProperty',
|
||||
CompletionItemKind::PROPERTY,
|
||||
|
@ -68,6 +71,27 @@ class CompletionTest extends TestCase
|
|||
], 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()
|
||||
{
|
||||
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/property.php');
|
||||
|
@ -76,7 +100,7 @@ class CompletionTest extends TestCase
|
|||
new TextDocumentIdentifier($completionUri),
|
||||
new Position(3, 6)
|
||||
)->wait();
|
||||
$this->assertEquals(new CompletionList([
|
||||
$this->assertCompletionsListSubset(new CompletionList([
|
||||
new CompletionItem(
|
||||
'testProperty',
|
||||
CompletionItemKind::PROPERTY,
|
||||
|
@ -100,7 +124,7 @@ class CompletionTest extends TestCase
|
|||
new TextDocumentIdentifier($completionUri),
|
||||
new Position(8, 5)
|
||||
)->wait();
|
||||
$this->assertEquals(new CompletionList([
|
||||
$this->assertCompletionsListSubset(new CompletionList([
|
||||
new CompletionItem(
|
||||
'$var',
|
||||
CompletionItemKind::VARIABLE,
|
||||
|
@ -132,7 +156,7 @@ class CompletionTest extends TestCase
|
|||
new TextDocumentIdentifier($completionUri),
|
||||
new Position(8, 6)
|
||||
)->wait();
|
||||
$this->assertEquals(new CompletionList([
|
||||
$this->assertCompletionsListSubset(new CompletionList([
|
||||
new CompletionItem(
|
||||
'$param',
|
||||
CompletionItemKind::VARIABLE,
|
||||
|
@ -154,13 +178,18 @@ class CompletionTest extends TestCase
|
|||
new TextDocumentIdentifier($completionUri),
|
||||
new Position(6, 10)
|
||||
)->wait();
|
||||
$this->assertEquals(new CompletionList([
|
||||
$this->assertCompletionsListSubset(new CompletionList([
|
||||
// Global TestClass definition (inserted as \TestClass)
|
||||
new CompletionItem(
|
||||
'TestClass',
|
||||
CompletionItemKind::CLASS_,
|
||||
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,
|
||||
'\TestClass'
|
||||
|
@ -179,7 +208,12 @@ class CompletionTest extends TestCase
|
|||
'TestClass',
|
||||
CompletionItemKind::CLASS_,
|
||||
'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,
|
||||
'TestClass'
|
||||
|
@ -193,6 +227,15 @@ class CompletionTest extends TestCase
|
|||
null,
|
||||
'\TestNamespace\ChildClass'
|
||||
),
|
||||
new CompletionItem(
|
||||
'Example',
|
||||
CompletionItemKind::CLASS_,
|
||||
'TestNamespace',
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
'\TestNamespace\Example'
|
||||
)
|
||||
], true), $items);
|
||||
}
|
||||
|
||||
|
@ -204,12 +247,17 @@ class CompletionTest extends TestCase
|
|||
new TextDocumentIdentifier($completionUri),
|
||||
new Position(6, 5)
|
||||
)->wait();
|
||||
$this->assertEquals(new CompletionList([
|
||||
$this->assertCompletionsListSubset(new CompletionList([
|
||||
new CompletionItem(
|
||||
'TestClass',
|
||||
CompletionItemKind::CLASS_,
|
||||
'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);
|
||||
}
|
||||
|
@ -222,7 +270,7 @@ class CompletionTest extends TestCase
|
|||
new TextDocumentIdentifier($completionUri),
|
||||
new Position(2, 14)
|
||||
)->wait();
|
||||
$this->assertEquals(new CompletionList([
|
||||
$this->assertCompletionsListSubset(new CompletionList([
|
||||
new CompletionItem(
|
||||
'staticTestProperty',
|
||||
CompletionItemKind::PROPERTY,
|
||||
|
@ -243,7 +291,7 @@ class CompletionTest extends TestCase
|
|||
new TextDocumentIdentifier($completionUri),
|
||||
new Position(2, 11)
|
||||
)->wait();
|
||||
$this->assertEquals(new CompletionList([
|
||||
$this->assertCompletionsListSubset(new CompletionList([
|
||||
new CompletionItem(
|
||||
'TEST_CLASS_CONST',
|
||||
CompletionItemKind::VARIABLE,
|
||||
|
@ -276,7 +324,7 @@ class CompletionTest extends TestCase
|
|||
new TextDocumentIdentifier($completionUri),
|
||||
new Position(2, 13)
|
||||
)->wait();
|
||||
$this->assertEquals(new CompletionList([
|
||||
$this->assertCompletionsListSubset(new CompletionList([
|
||||
new CompletionItem(
|
||||
'TEST_CLASS_CONST',
|
||||
CompletionItemKind::VARIABLE,
|
||||
|
@ -309,7 +357,7 @@ class CompletionTest extends TestCase
|
|||
new TextDocumentIdentifier($completionUri),
|
||||
new Position(2, 13)
|
||||
)->wait();
|
||||
$this->assertEquals(new CompletionList([
|
||||
$this->assertCompletionsListSubset(new CompletionList([
|
||||
new CompletionItem(
|
||||
'TEST_CLASS_CONST',
|
||||
CompletionItemKind::VARIABLE,
|
||||
|
@ -342,12 +390,17 @@ class CompletionTest extends TestCase
|
|||
new TextDocumentIdentifier($completionUri),
|
||||
new Position(6, 6)
|
||||
)->wait();
|
||||
$this->assertEquals(new CompletionList([
|
||||
$this->assertCompletionsListSubset(new CompletionList([
|
||||
new CompletionItem(
|
||||
'TestClass',
|
||||
CompletionItemKind::CLASS_,
|
||||
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,
|
||||
'TestClass'
|
||||
|
@ -363,7 +416,7 @@ class CompletionTest extends TestCase
|
|||
new TextDocumentIdentifier($completionUri),
|
||||
new Position(2, 1)
|
||||
)->wait();
|
||||
$this->assertEquals(new CompletionList([
|
||||
$this->assertCompletionsListSubset(new CompletionList([
|
||||
new CompletionItem('class', CompletionItemKind::KEYWORD, null, null, null, null, 'class'),
|
||||
new CompletionItem('clone', CompletionItemKind::KEYWORD, null, null, null, null, 'clone')
|
||||
], true), $items);
|
||||
|
@ -377,7 +430,7 @@ class CompletionTest extends TestCase
|
|||
new TextDocumentIdentifier($completionUri),
|
||||
new Position(0, 0)
|
||||
)->wait();
|
||||
$this->assertEquals(new CompletionList([
|
||||
$this->assertCompletionsListSubset(new CompletionList([
|
||||
new CompletionItem(
|
||||
'<?php',
|
||||
CompletionItemKind::KEYWORD,
|
||||
|
@ -391,7 +444,7 @@ class CompletionTest extends TestCase
|
|||
], true), $items);
|
||||
}
|
||||
|
||||
public function testHtmlWithPrefix()
|
||||
public function testHtmlWontBeProposedWithoutCompletionContext()
|
||||
{
|
||||
$completionUri = pathToUri(__DIR__ . '/../../../fixtures/completion/html_with_prefix.php');
|
||||
$this->loader->open($completionUri, file_get_contents($completionUri));
|
||||
|
@ -399,6 +452,55 @@ class CompletionTest extends TestCase
|
|||
new TextDocumentIdentifier($completionUri),
|
||||
new Position(0, 1)
|
||||
)->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([
|
||||
new CompletionItem(
|
||||
'<?php',
|
||||
|
@ -421,7 +523,7 @@ class CompletionTest extends TestCase
|
|||
new TextDocumentIdentifier($completionUri),
|
||||
new Position(4, 6)
|
||||
)->wait();
|
||||
$this->assertEquals(new CompletionList([
|
||||
$this->assertCompletionsListSubset(new CompletionList([
|
||||
new CompletionItem(
|
||||
'SomeNamespace',
|
||||
CompletionItemKind::MODULE,
|
||||
|
@ -442,7 +544,7 @@ class CompletionTest extends TestCase
|
|||
new TextDocumentIdentifier($completionUri),
|
||||
new Position(4, 8)
|
||||
)->wait();
|
||||
$this->assertEquals(new CompletionList([
|
||||
$this->assertCompletionsListSubset(new CompletionList([
|
||||
new CompletionItem(
|
||||
'$abc2',
|
||||
CompletionItemKind::VARIABLE,
|
||||
|
@ -465,4 +567,323 @@ class CompletionTest extends TestCase
|
|||
)
|
||||
], 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,11 +5,12 @@ namespace LanguageServer\Tests\Server\TextDocument\Definition;
|
|||
|
||||
use LanguageServer\Tests\MockProtocolStream;
|
||||
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\ContentRetriever\FileSystemContentRetriever;
|
||||
use LanguageServer\Protocol\{TextDocumentIdentifier, Position, Range, Location, ClientCapabilities};
|
||||
use Sabre\Event\Promise;
|
||||
use LanguageServer\Protocol\{TextDocumentIdentifier, Position, Range, Location};
|
||||
|
||||
class GlobalFallbackTest extends ServerTestCase
|
||||
{
|
||||
|
|
|
@ -24,16 +24,28 @@ class GlobalTest extends ServerTestCase
|
|||
// namespace keyword
|
||||
$result = $this->textDocument->definition(
|
||||
new TextDocumentIdentifier(pathToUri(realpath(__DIR__ . '/../../../../fixtures/references.php'))),
|
||||
new Position(2, 4)
|
||||
new Position(1, 0)
|
||||
)->wait();
|
||||
$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()
|
||||
{
|
||||
// $obj = new TestClass();
|
||||
// Get definition for TestClass
|
||||
$reference = $this->getReferenceLocations('TestClass')[0];
|
||||
$reference = $this->getReferenceLocations('TestClass')[1];
|
||||
$result = $this->textDocument->definition(
|
||||
new TextDocumentIdentifier($reference->uri),
|
||||
$reference->range->start
|
||||
|
@ -45,7 +57,7 @@ class GlobalTest extends ServerTestCase
|
|||
{
|
||||
// TestClass::staticTestMethod();
|
||||
// Get definition for TestClass
|
||||
$reference = $this->getReferenceLocations('TestClass')[1];
|
||||
$reference = $this->getReferenceLocations('TestClass')[2];
|
||||
$result = $this->textDocument->definition(
|
||||
new TextDocumentIdentifier($reference->uri),
|
||||
$reference->range->start
|
||||
|
@ -57,7 +69,7 @@ class GlobalTest extends ServerTestCase
|
|||
{
|
||||
// echo TestClass::$staticTestProperty;
|
||||
// Get definition for TestClass
|
||||
$reference = $this->getReferenceLocations('TestClass')[2];
|
||||
$reference = $this->getReferenceLocations('TestClass')[3];
|
||||
$result = $this->textDocument->definition(
|
||||
new TextDocumentIdentifier($reference->uri),
|
||||
$reference->range->start
|
||||
|
@ -69,7 +81,7 @@ class GlobalTest extends ServerTestCase
|
|||
{
|
||||
// TestClass::TEST_CLASS_CONST;
|
||||
// Get definition for TestClass
|
||||
$reference = $this->getReferenceLocations('TestClass')[3];
|
||||
$reference = $this->getReferenceLocations('TestClass')[4];
|
||||
$result = $this->textDocument->definition(
|
||||
new TextDocumentIdentifier($reference->uri),
|
||||
$reference->range->start
|
||||
|
@ -213,7 +225,7 @@ class GlobalTest extends ServerTestCase
|
|||
{
|
||||
// function whatever(TestClass $param) {
|
||||
// Get definition for TestClass
|
||||
$reference = $this->getReferenceLocations('TestClass')[4];
|
||||
$reference = $this->getReferenceLocations('TestClass')[5];
|
||||
$result = $this->textDocument->definition(
|
||||
new TextDocumentIdentifier($reference->uri),
|
||||
$reference->range->start
|
||||
|
@ -225,7 +237,7 @@ class GlobalTest extends ServerTestCase
|
|||
{
|
||||
// function whatever(TestClass $param): TestClass {
|
||||
// Get definition for TestClass
|
||||
$reference = $this->getReferenceLocations('TestClass')[5];
|
||||
$reference = $this->getReferenceLocations('TestClass')[6];
|
||||
$result = $this->textDocument->definition(
|
||||
new TextDocumentIdentifier($reference->uri),
|
||||
$reference->range->start
|
||||
|
|
|
@ -34,7 +34,7 @@ class NamespacedTest extends GlobalTest
|
|||
{
|
||||
// use TestNamespace\TestClass;
|
||||
// Get definition for TestClass
|
||||
$reference = $this->getReferenceLocations('TestClass')[6];
|
||||
$reference = $this->getReferenceLocations('TestClass')[7];
|
||||
$result = $this->textDocument->definition(
|
||||
new TextDocumentIdentifier($reference->uri),
|
||||
$reference->range->start
|
||||
|
@ -46,7 +46,7 @@ class NamespacedTest extends GlobalTest
|
|||
{
|
||||
// use TestNamespace\{TestTrait, TestInterface};
|
||||
// Get definition for TestInterface
|
||||
$reference = $this->getReferenceLocations('TestClass')[0];
|
||||
$reference = $this->getReferenceLocations('TestClass')[1];
|
||||
$result = $this->textDocument->definition(
|
||||
new TextDocumentIdentifier($reference->uri),
|
||||
$reference->range->start
|
||||
|
|
|
@ -5,17 +5,16 @@ namespace LanguageServer\Tests\Server\TextDocument;
|
|||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use LanguageServer\Tests\MockProtocolStream;
|
||||
use LanguageServer\{Server, Client, LanguageClient, PhpDocumentLoader, DefinitionResolver};
|
||||
use LanguageServer\{
|
||||
Server, LanguageClient, PhpDocumentLoader, DefinitionResolver
|
||||
};
|
||||
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
|
||||
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
|
||||
use LanguageServer\Protocol\{
|
||||
TextDocumentIdentifier,
|
||||
TextDocumentItem,
|
||||
VersionedTextDocumentIdentifier,
|
||||
TextDocumentContentChangeEvent,
|
||||
Range,
|
||||
Position,
|
||||
ClientCapabilities
|
||||
Position
|
||||
};
|
||||
|
||||
class DidChangeTest extends TestCase
|
||||
|
|
|
@ -5,11 +5,12 @@ namespace LanguageServer\Tests\Server\TextDocument;
|
|||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use LanguageServer\Tests\MockProtocolStream;
|
||||
use LanguageServer\{Server, Client, LanguageClient, PhpDocumentLoader, DefinitionResolver};
|
||||
use LanguageServer\{
|
||||
Server, LanguageClient, PhpDocumentLoader, DefinitionResolver
|
||||
};
|
||||
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
|
||||
use LanguageServer\Index\{Index, ProjectIndex, DependenciesIndex};
|
||||
use LanguageServer\Protocol\{TextDocumentItem, TextDocumentIdentifier, ClientCapabilities};
|
||||
use Exception;
|
||||
use LanguageServer\Protocol\{TextDocumentItem, TextDocumentIdentifier};
|
||||
|
||||
class DidCloseTest extends TestCase
|
||||
{
|
||||
|
|
|
@ -30,6 +30,9 @@ class DocumentSymbolTest extends ServerTestCase
|
|||
new SymbolInformation('TestInterface', SymbolKind::INTERFACE, $this->getDefinitionLocation('TestNamespace\\TestInterface'), '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('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);
|
||||
// @codingStandardsIgnoreEnd
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue