Compare commits
111 Commits
Author | SHA1 | Date |
---|---|---|
|
fb48c70ce2 | |
|
3fc105717d | |
|
9dc1656592 | |
|
7303143a60 | |
|
1705583e32 | |
|
1da3328bc2 | |
|
450116e2f3 | |
|
b1cc565d7e | |
|
ed2d8ddb1e | |
|
680f430453 | |
|
c7d25c7b44 | |
|
71390c9903 | |
|
24388bcf26 | |
|
18c6ccd137 | |
|
3d8318bd03 | |
|
b4b4a2fff5 | |
|
3931c8848f | |
|
26e3451e61 | |
|
fe33c8cd7f | |
|
7e1ca75863 | |
|
ebf4c096b3 | |
|
49f1e8f04a | |
|
02b7d2fdb6 | |
|
de1af6a165 | |
|
e10896f905 | |
|
b412c125a4 | |
|
8adcf92c2f | |
|
fc6b069425 | |
|
c5a83af327 | |
|
a8f60c9cf6 | |
|
d9bc0b0285 | |
|
6894d85aaf | |
|
c48ee55808 | |
|
20960a8b9f | |
|
8439da999a | |
|
1cfba8b6bb | |
|
425b2390b5 | |
|
a0caf8d18f | |
|
63da051e72 | |
|
9eea26df71 | |
|
f46fccd0d3 | |
|
6d0a7ba7df | |
|
a40cf731f7 | |
|
78316545a8 | |
|
09477b747e | |
|
9b1fafae58 | |
|
ff746a836d | |
|
31bae23912 | |
|
724eb6f1dc | |
|
4f672c24d8 | |
|
80ef8ff503 | |
|
b1a1875070 | |
|
06747bb734 | |
|
607cd8158d | |
|
1ec8d8d8e2 | |
|
0afc3320d5 | |
|
1804ac8d97 | |
|
9434cb1b67 | |
|
0e645301cc | |
|
3e41244b6f | |
|
eadf305a1f | |
|
d54ece3366 | |
|
857fe26eb5 | |
|
b4a3134e2a | |
|
f5c45f83ed | |
|
b03b9a239c | |
|
41e84880b3 | |
|
74578c7b58 | |
|
235a790156 | |
|
db484617b6 | |
|
f00fd1b62c | |
|
e9fc97d430 | |
|
6dbeef63bc | |
|
ac6bce929f | |
|
d3c9133892 | |
|
1edbe35609 | |
|
744062c14e | |
|
7ae6452d1a | |
|
c74076d84f | |
|
99d8a361db | |
|
9e551a310b | |
|
b86d6c96c7 | |
|
95f49d3a70 | |
|
fbaa7b3cc5 | |
|
1db6b7bbb3 | |
|
16cf8f53e9 | |
|
4384d49414 | |
|
a934aff7a9 | |
|
7b1176dd9d | |
|
1240f25e01 | |
|
19bf94ac7b | |
|
e31f7b5923 | |
|
0c399150a3 | |
|
b9ebfb52c9 | |
|
3d8655d504 | |
|
d24c42008e | |
|
d4443465bb | |
|
a4739430f8 | |
|
63bf43e40c | |
|
7ce2284176 | |
|
35f33c8c91 | |
|
94fc0405fd | |
|
fc0bf4c163 | |
|
fced1d5af6 | |
|
00552120ad | |
|
f43ce50d5a | |
|
08fe84de35 | |
|
a454cd2873 | |
|
dae3f2576c | |
|
f97105740d | |
|
548120314d |
|
@ -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,24 @@
|
|||
* 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
|
||||
/.phan 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,8 +1,8 @@
|
|||
.DS_Store
|
||||
.vscode
|
||||
.idea
|
||||
vendor/
|
||||
.phpls/
|
||||
composer.lock
|
||||
stubs
|
||||
*.ast
|
||||
*.ast
|
||||
node_modules/
|
||||
|
|
|
@ -0,0 +1,308 @@
|
|||
<?php
|
||||
|
||||
use Phan\Issue;
|
||||
|
||||
/**
|
||||
* This configuration file was automatically generated by 'phan --init --init-level=1'
|
||||
*
|
||||
* TODOs (added by 'phan --init'):
|
||||
*
|
||||
* - Go through this file and verify that there are no missing/unnecessary files/directories.
|
||||
* (E.g. this only includes direct composer dependencies - You may have to manually add indirect composer dependencies to 'directory_list')
|
||||
* - Look at 'plugins' and add or remove plugins if appropriate (see https://github.com/phan/phan/tree/master/.phan/plugins#plugins)
|
||||
* - Add global suppressions for pre-existing issues to suppress_issue_types (https://github.com/phan/phan/wiki/Tutorial-for-Analyzing-a-Large-Sloppy-Code-Base)
|
||||
*
|
||||
* This configuration will be read and overlayed on top of the
|
||||
* default configuration. Command line arguments will be applied
|
||||
* after this file is read.
|
||||
*
|
||||
* @see src/Phan/Config.php
|
||||
* See Config for all configurable options.
|
||||
*
|
||||
* A Note About Paths
|
||||
* ==================
|
||||
*
|
||||
* Files referenced from this file should be defined as
|
||||
*
|
||||
* ```
|
||||
* Config::projectPath('relative_path/to/file')
|
||||
* ```
|
||||
*
|
||||
* where the relative path is relative to the root of the
|
||||
* project which is defined as either the working directory
|
||||
* of the phan executable or a path passed in via the CLI
|
||||
* '-d' flag.
|
||||
*/
|
||||
return [
|
||||
|
||||
// Supported values: '7.0', '7.1', '7.2', null.
|
||||
// If this is set to null,
|
||||
// then Phan assumes the PHP version which is closest to the minor version
|
||||
// of the php executable used to execute phan.
|
||||
// Automatically inferred from composer.json requirement for "php" of "^7.0"
|
||||
'target_php_version' => '7.0',
|
||||
|
||||
// If enabled, missing properties will be created when
|
||||
// they are first seen. If false, we'll report an
|
||||
// error message if there is an attempt to write
|
||||
// to a class property that wasn't explicitly
|
||||
// defined.
|
||||
'allow_missing_properties' => false,
|
||||
|
||||
// If enabled, null can be cast as any type and any
|
||||
// type can be cast to null. Setting this to true
|
||||
// will cut down on false positives.
|
||||
'null_casts_as_any_type' => false,
|
||||
|
||||
// If enabled, allow null to be cast as any array-like type.
|
||||
// This is an incremental step in migrating away from null_casts_as_any_type.
|
||||
// If null_casts_as_any_type is true, this has no effect.
|
||||
'null_casts_as_array' => false,
|
||||
|
||||
// If enabled, allow any array-like type to be cast to null.
|
||||
// This is an incremental step in migrating away from null_casts_as_any_type.
|
||||
// If null_casts_as_any_type is true, this has no effect.
|
||||
'array_casts_as_null' => false,
|
||||
|
||||
// If enabled, scalars (int, float, bool, string, null)
|
||||
// are treated as if they can cast to each other.
|
||||
// This does not affect checks of array keys. See scalar_array_key_cast.
|
||||
'scalar_implicit_cast' => false,
|
||||
|
||||
// If enabled, any scalar array keys (int, string)
|
||||
// are treated as if they can cast to each other.
|
||||
// E.g. array<int,stdClass> can cast to array<string,stdClass> and vice versa.
|
||||
// Normally, a scalar type such as int could only cast to/from int and mixed.
|
||||
'scalar_array_key_cast' => false,
|
||||
|
||||
// If this has entries, scalars (int, float, bool, string, null)
|
||||
// are allowed to perform the casts listed.
|
||||
// E.g. ['int' => ['float', 'string'], 'float' => ['int'], 'string' => ['int'], 'null' => ['string']]
|
||||
// allows casting null to a string, but not vice versa.
|
||||
// (subset of scalar_implicit_cast)
|
||||
'scalar_implicit_partial' => [],
|
||||
|
||||
// If true, seemingly undeclared variables in the global
|
||||
// scope will be ignored. This is useful for projects
|
||||
// with complicated cross-file globals that you have no
|
||||
// hope of fixing.
|
||||
'ignore_undeclared_variables_in_global_scope' => false,
|
||||
|
||||
// Backwards Compatibility Checking. This is slow
|
||||
// and expensive, but you should consider running
|
||||
// it before upgrading your version of PHP to a
|
||||
// new version that has backward compatibility
|
||||
// breaks.
|
||||
'backward_compatibility_checks' => false,
|
||||
|
||||
// If true, check to make sure the return type declared
|
||||
// in the doc-block (if any) matches the return type
|
||||
// declared in the method signature.
|
||||
'check_docblock_signature_return_type_match' => true,
|
||||
|
||||
// (*Requires check_docblock_signature_param_type_match to be true*)
|
||||
// If true, make narrowed types from phpdoc params override
|
||||
// the real types from the signature, when real types exist.
|
||||
// (E.g. allows specifying desired lists of subclasses,
|
||||
// or to indicate a preference for non-nullable types over nullable types)
|
||||
// Affects analysis of the body of the method and the param types passed in by callers.
|
||||
'prefer_narrowed_phpdoc_param_type' => true,
|
||||
|
||||
// (*Requires check_docblock_signature_return_type_match to be true*)
|
||||
// If true, make narrowed types from phpdoc returns override
|
||||
// the real types from the signature, when real types exist.
|
||||
// (E.g. allows specifying desired lists of subclasses,
|
||||
// or to indicate a preference for non-nullable types over nullable types)
|
||||
// Affects analysis of return statements in the body of the method and the return types passed in by callers.
|
||||
'prefer_narrowed_phpdoc_return_type' => true,
|
||||
|
||||
'ensure_signature_compatibility' => true,
|
||||
|
||||
// Set to true in order to attempt to detect dead
|
||||
// (unreferenced) code. Keep in mind that the
|
||||
// results will only be a guess given that classes,
|
||||
// properties, constants and methods can be referenced
|
||||
// as variables (like `$class->$property` or
|
||||
// `$class->$method()`) in ways that we're unable
|
||||
// to make sense of.
|
||||
'dead_code_detection' => false,
|
||||
|
||||
// If true, this run a quick version of checks that takes less
|
||||
// time at the cost of not running as thorough
|
||||
// an analysis. You should consider setting this
|
||||
// to true only when you wish you had more **undiagnosed** issues
|
||||
// to fix in your code base.
|
||||
//
|
||||
// In quick-mode the scanner doesn't rescan a function
|
||||
// or a method's code block every time a call is seen.
|
||||
// This means that the problem here won't be detected:
|
||||
//
|
||||
// ```php
|
||||
// <?php
|
||||
// function test($arg):int {
|
||||
// return $arg;
|
||||
// }
|
||||
// test("abc");
|
||||
// ```
|
||||
//
|
||||
// This would normally generate:
|
||||
//
|
||||
// ```sh
|
||||
// test.php:3 TypeError return string but `test()` is declared to return int
|
||||
// ```
|
||||
//
|
||||
// The initial scan of the function's code block has no
|
||||
// type information for `$arg`. It isn't until we see
|
||||
// the call and rescan test()'s code block that we can
|
||||
// detect that it is actually returning the passed in
|
||||
// `string` instead of an `int` as declared.
|
||||
'quick_mode' => false,
|
||||
|
||||
// If true, then before analysis, try to simplify AST into a form
|
||||
// which improves Phan's type inference in edge cases.
|
||||
//
|
||||
// This may conflict with 'dead_code_detection'.
|
||||
// When this is true, this slows down analysis slightly.
|
||||
//
|
||||
// E.g. rewrites `if ($a = value() && $a > 0) {...}`
|
||||
// into $a = value(); if ($a) { if ($a > 0) {...}}`
|
||||
'simplify_ast' => true,
|
||||
|
||||
// Enable or disable support for generic templated
|
||||
// class types.
|
||||
'generic_types_enabled' => true,
|
||||
|
||||
// Override to hardcode existence and types of (non-builtin) globals in the global scope.
|
||||
// Class names should be prefixed with '\\'.
|
||||
// (E.g. ['_FOO' => '\\FooClass', 'page' => '\\PageClass', 'userId' => 'int'])
|
||||
'globals_type_map' => [],
|
||||
|
||||
// The minimum severity level to report on. This can be
|
||||
// set to Issue::SEVERITY_LOW, Issue::SEVERITY_NORMAL or
|
||||
// Issue::SEVERITY_CRITICAL. Setting it to only
|
||||
// critical issues is a good place to start on a big
|
||||
// sloppy mature code base.
|
||||
'minimum_severity' => Issue::SEVERITY_LOW,
|
||||
|
||||
// Add any issue types (such as 'PhanUndeclaredMethod')
|
||||
// to this black-list to inhibit them from being reported.
|
||||
'suppress_issue_types' => [
|
||||
'PhanTypeMismatchDeclaredParamNullable',
|
||||
'PhanUndeclaredProperty', // 66 occurence(s) (e.g. not being specific enough about the subclass)
|
||||
'PhanUndeclaredMethod', // 32 occurence(s) (e.g. not being specific enough about the subclass of Node)
|
||||
'PhanTypeMismatchArgument', // 21 occurence(s)
|
||||
'PhanTypeMismatchProperty', // 13 occurence(s)
|
||||
'PhanUnreferencedUseNormal', // 10 occurence(s) TODO: Fix
|
||||
'PhanTypeMismatchDeclaredReturn', // 8 occurence(s)
|
||||
'PhanUndeclaredTypeProperty', // 7 occurence(s)
|
||||
'PhanTypeMismatchReturn', // 6 occurence(s)
|
||||
'PhanUndeclaredVariable', // 4 occurence(s)
|
||||
'PhanUndeclaredTypeReturnType', // 4 occurence(s)
|
||||
'PhanParamTooMany', // 3 occurence(s)
|
||||
'PhanUndeclaredTypeParameter', // 2 occurence(s)
|
||||
'PhanUndeclaredClassProperty', // 2 occurence(s)
|
||||
'PhanTypeSuspiciousStringExpression', // 2 occurence(s)
|
||||
'PhanTypeMismatchArgumentInternal', // 2 occurence(s)
|
||||
'PhanUnextractableAnnotationElementName', // 1 occurence(s)
|
||||
'PhanUndeclaredClassMethod', // 1 occurence(s)
|
||||
'PhanUndeclaredClassInstanceof', // 1 occurence(s)
|
||||
'PhanTypeSuspiciousNonTraversableForeach', // 1 occurence(s)
|
||||
'PhanTypeMismatchDimAssignment', // 1 occurence(s)
|
||||
'PhanTypeMismatchDeclaredParam', // 1 occurence(s)
|
||||
'PhanTypeInvalidDimOffset', // 1 occurence(s)
|
||||
],
|
||||
|
||||
// A regular expression to match files to be excluded
|
||||
// from parsing and analysis and will not be read at all.
|
||||
//
|
||||
// This is useful for excluding groups of test or example
|
||||
// directories/files, unanalyzable files, or files that
|
||||
// can't be removed for whatever reason.
|
||||
// (e.g. '@Test\.php$@', or '@vendor/.*/(tests|Tests)/@')
|
||||
'exclude_file_regex' => '@^vendor/.*/(tests?|Tests?)/@',
|
||||
|
||||
// A file list that defines files that will be excluded
|
||||
// from parsing and analysis and will not be read at all.
|
||||
//
|
||||
// This is useful for excluding hopelessly unanalyzable
|
||||
// files that can't be removed for whatever reason.
|
||||
'exclude_file_list' => [],
|
||||
|
||||
// A directory list that defines files that will be excluded
|
||||
// from static analysis, but whose class and method
|
||||
// information should be included.
|
||||
//
|
||||
// Generally, you'll want to include the directories for
|
||||
// third-party code (such as "vendor/") in this list.
|
||||
//
|
||||
// n.b.: If you'd like to parse but not analyze 3rd
|
||||
// party code, directories containing that code
|
||||
// should be added to the `directory_list` as
|
||||
// to `excluce_analysis_directory_list`.
|
||||
'exclude_analysis_directory_list' => [
|
||||
'vendor/',
|
||||
],
|
||||
|
||||
// The number of processes to fork off during the analysis
|
||||
// phase.
|
||||
'processes' => 1,
|
||||
|
||||
// List of case-insensitive file extensions supported by Phan.
|
||||
// (e.g. php, html, htm)
|
||||
'analyzed_file_extensions' => [
|
||||
'php',
|
||||
],
|
||||
|
||||
// You can put paths to stubs of internal extensions in this config option.
|
||||
// If the corresponding extension is **not** loaded, then phan will use the stubs instead.
|
||||
// Phan will continue using its detailed type annotations,
|
||||
// but load the constants, classes, functions, and classes (and their Reflection types)
|
||||
// from these stub files (doubling as valid php files).
|
||||
// Use a different extension from php to avoid accidentally loading these.
|
||||
// The 'tools/make_stubs' script can be used to generate your own stubs (compatible with php 7.0+ right now)
|
||||
'autoload_internal_extension_signatures' => [],
|
||||
|
||||
// A list of plugin files to execute
|
||||
// Plugins which are bundled with Phan can be added here by providing their name (e.g. 'AlwaysReturnPlugin')
|
||||
// Alternately, you can pass in the full path to a PHP file with the plugin's implementation (e.g. 'vendor/phan/phan/.phan/plugins/AlwaysReturnPlugin.php')
|
||||
'plugins' => [
|
||||
'AlwaysReturnPlugin',
|
||||
'DollarDollarPlugin',
|
||||
'DuplicateArrayKeyPlugin',
|
||||
'PregRegexCheckerPlugin',
|
||||
'PrintfCheckerPlugin',
|
||||
'UnreachableCodePlugin',
|
||||
],
|
||||
|
||||
// A list of directories that should be parsed for class and
|
||||
// method information. After excluding the directories
|
||||
// defined in exclude_analysis_directory_list, the remaining
|
||||
// files will be statically analyzed for errors.
|
||||
//
|
||||
// Thus, both first-party and third-party code being used by
|
||||
// your application should be included in this list.
|
||||
'directory_list' => [
|
||||
'src',
|
||||
'vendor/composer/xdebug-handler/src',
|
||||
'vendor/felixfbecker/advanced-json-rpc/lib',
|
||||
'vendor/felixfbecker/language-server-protocol/src/',
|
||||
'vendor/microsoft/tolerant-php-parser/src',
|
||||
'vendor/netresearch/jsonmapper/src',
|
||||
'vendor/phpdocumentor/reflection-common/src',
|
||||
'vendor/phpdocumentor/reflection-docblock/src',
|
||||
'vendor/phpdocumentor/type-resolver/src',
|
||||
'vendor/phpunit/phpunit/src',
|
||||
'vendor/psr/log/Psr',
|
||||
'vendor/sabre/event/lib',
|
||||
'vendor/sabre/uri/lib',
|
||||
'vendor/webmozart/glob/src',
|
||||
'vendor/webmozart/path-util/src',
|
||||
],
|
||||
|
||||
// A list of individual files to include in analysis
|
||||
// with a path relative to the root directory of the
|
||||
// project
|
||||
'file_list' => [
|
||||
'bin/php-language-server.php',
|
||||
],
|
||||
];
|
64
.travis.yml
64
.travis.yml
|
@ -1,28 +1,60 @@
|
|||
language: php
|
||||
|
||||
php:
|
||||
- '7.0'
|
||||
- '7.0'
|
||||
- '7.2'
|
||||
|
||||
services:
|
||||
- docker
|
||||
git:
|
||||
depth: 10
|
||||
submodules: false
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.composer/cache
|
||||
- $HOME/Library/Caches/Homebrew
|
||||
- $HOME/.composer/cache
|
||||
- $HOME/.npm
|
||||
|
||||
install:
|
||||
- composer install
|
||||
- composer run-script parse-stubs
|
||||
- composer install --prefer-dist --no-interaction
|
||||
- pecl install ast-1.0.0
|
||||
|
||||
script:
|
||||
- vendor/bin/phpcs -n
|
||||
- vendor/bin/phpunit --coverage-clover=coverage.xml
|
||||
- vendor/bin/phpcs -n
|
||||
- vendor/bin/phan
|
||||
- vendor/bin/phpunit --coverage-clover=coverage.xml --colors=always
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
after_success:
|
||||
- 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
|
||||
}
|
||||
}
|
15
Dockerfile
15
Dockerfile
|
@ -1,22 +1,19 @@
|
|||
|
||||
# Running this container will start a language server that listens for TCP connections on port 2088
|
||||
# Every connection will be run in a forked child process
|
||||
|
||||
# Please note that before building the image, you have to install dependencies with `composer install`
|
||||
FROM composer AS builder
|
||||
|
||||
COPY ./ /app
|
||||
RUN composer install
|
||||
|
||||
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/*
|
||||
LABEL maintainer="Felix Becker <felix.b@outlook.com>"
|
||||
|
||||
RUN docker-php-ext-configure pcntl --enable-pcntl
|
||||
RUN docker-php-ext-install pcntl
|
||||
COPY ./php.ini /usr/local/etc/php/conf.d/
|
||||
|
||||
COPY ./ /srv/phpls
|
||||
COPY --from=builder /app /srv/phpls
|
||||
|
||||
WORKDIR /srv/phpls
|
||||
|
||||
|
|
46
README.md
46
README.md
|
@ -1,9 +1,10 @@
|
|||
# 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 +12,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 +57,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 +184,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:
|
||||
|
||||
|
@ -181,6 +195,7 @@ Example:
|
|||
- [Eclipse Che](https://eclipse.org/che/)
|
||||
- [Eclipse IDE (LSP4E-PHP)](https://github.com/eclipselabs/lsp4e-php)
|
||||
- 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 +205,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'
|
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Tests;
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use Composer\XdebugHandler\XdebugHandler;
|
||||
use Exception;
|
||||
use LanguageServer\CompletionProvider;
|
||||
use LanguageServer\DefinitionResolver;
|
||||
use LanguageServer\Index\Index;
|
||||
use LanguageServer\PhpDocument;
|
||||
use LanguageServer\StderrLogger;
|
||||
use LanguageServerProtocol\Position;
|
||||
use Microsoft\PhpParser;
|
||||
use phpDocumentor\Reflection\DocBlockFactory;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
|
||||
$logger = new StderrLogger();
|
||||
$xdebugHandler = new XdebugHandler('PHPLS');
|
||||
$xdebugHandler->setLogger($logger);
|
||||
$xdebugHandler->check();
|
||||
unset($xdebugHandler);
|
||||
|
||||
$totalSize = 0;
|
||||
|
||||
$framework = "symfony";
|
||||
|
||||
$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->getRealPath();
|
||||
}
|
||||
}
|
||||
|
||||
if (count($testProviderArray) === 0) {
|
||||
throw new Exception("ERROR: Validation testsuite frameworks not found - run `git submodule update --init --recursive` to download.");
|
||||
}
|
||||
|
||||
$index = new Index;
|
||||
$definitionResolver = new DefinitionResolver($index);
|
||||
$completionProvider = new CompletionProvider($definitionResolver, $index);
|
||||
$docBlockFactory = DocBlockFactory::createInstance();
|
||||
$completionFile = realpath(__DIR__ . '/../validation/frameworks/symfony/src/Symfony/Component/HttpFoundation/Request.php');
|
||||
$parser = new PhpParser\Parser();
|
||||
$completionDocument = null;
|
||||
|
||||
echo "Indexing $framework" . PHP_EOL;
|
||||
|
||||
foreach ($testProviderArray as $idx => $testCaseFile) {
|
||||
if (filesize($testCaseFile) > 100000) {
|
||||
continue;
|
||||
}
|
||||
if ($idx % 100 === 0) {
|
||||
echo $idx . '/' . count($testProviderArray) . PHP_EOL;
|
||||
}
|
||||
|
||||
$fileContents = file_get_contents($testCaseFile);
|
||||
|
||||
try {
|
||||
$d = new PhpDocument($testCaseFile, $fileContents, $index, $parser, $docBlockFactory, $definitionResolver);
|
||||
if ($testCaseFile === $completionFile) {
|
||||
$completionDocument = $d;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo $e->getMessage() . PHP_EOL;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
echo "Getting completion". PHP_EOL;
|
||||
|
||||
// Completion in $this->|request = new ParameterBag($request);
|
||||
$start = microtime(true);
|
||||
$list = $completionProvider->provideCompletion($completionDocument, new Position(274, 15));
|
||||
$end = microtime(true);
|
||||
echo 'Time ($this->|): ' . ($end - $start) . 's' . PHP_EOL;
|
||||
echo count($list->items) . ' completion items' . PHP_EOL;
|
||||
|
||||
// Completion in $this->request = new| ParameterBag($request);
|
||||
// (this only finds ParameterBag though.)
|
||||
$start = microtime(true);
|
||||
$list = $completionProvider->provideCompletion($completionDocument, new Position(274, 28));
|
||||
$end = microtime(true);
|
||||
echo 'Time (new|): ' . ($end - $start) . 's' . PHP_EOL;
|
||||
echo count($list->items) . ' completion items' . PHP_EOL;
|
|
@ -1,23 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Tests;
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use Composer\XdebugHandler\XdebugHandler;
|
||||
use Exception;
|
||||
use LanguageServer\DefinitionResolver;
|
||||
use LanguageServer\Index\Index;
|
||||
use LanguageServer\PhpDocument;
|
||||
use LanguageServer\DefinitionResolver;
|
||||
use LanguageServer\StderrLogger;
|
||||
use Microsoft\PhpParser;
|
||||
use phpDocumentor\Reflection\DocBlockFactory;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
|
||||
$logger = new StderrLogger();
|
||||
$xdebugHandler = new XdebugHandler('PHPLS');
|
||||
$xdebugHandler->setLogger($logger);
|
||||
$xdebugHandler->check();
|
||||
unset($xdebugHandler);
|
||||
|
||||
$totalSize = 0;
|
||||
|
||||
$frameworks = ["drupal", "wordpress", "php-language-server", "tolerant-php-parser", "math-php", "symfony", "CodeIgniter", "cakephp"];
|
||||
$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");
|
||||
$iterator = new RecursiveDirectoryIterator(__DIR__ . "/../validation/frameworks/$framework");
|
||||
$testProviderArray = array();
|
||||
|
||||
foreach (new RecursiveIteratorIterator($iterator) as $file) {
|
||||
|
@ -37,8 +45,8 @@ foreach($frameworks as $framework) {
|
|||
if (filesize($testCaseFile) > 10000) {
|
||||
continue;
|
||||
}
|
||||
if ($idx % 1000 === 0) {
|
||||
echo "$idx\n";
|
||||
if ($idx % 500 === 0) {
|
||||
echo $idx . '/' . count($testProviderArray) . PHP_EOL;
|
||||
}
|
||||
|
||||
$fileContents = file_get_contents($testCaseFile);
|
||||
|
@ -51,11 +59,7 @@ foreach($frameworks as $framework) {
|
|||
$definitionResolver = new DefinitionResolver($index);
|
||||
$parser = new PhpParser\Parser();
|
||||
|
||||
try {
|
||||
$document = new PhpDocument($testCaseFile, $fileContents, $index, $parser, $docBlockFactory, $definitionResolver);
|
||||
} catch (\Throwable $e) {
|
||||
continue;
|
||||
}
|
||||
$document = new PhpDocument($testCaseFile, $fileContents, $index, $parser, $docBlockFactory, $definitionResolver);
|
||||
}
|
||||
|
||||
echo "------------------------------\n";
|
|
@ -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",
|
||||
"name": "icedream/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,27 +14,35 @@
|
|||
"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",
|
||||
"phpdocumentor/reflection-docblock": "^3.0",
|
||||
"sabre/event": "^5.0",
|
||||
"felixfbecker/advanced-json-rpc": "^2.0",
|
||||
"squizlabs/php_codesniffer" : "^3.0",
|
||||
"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",
|
||||
"felixfbecker/language-server-protocol": "^1.0.1",
|
||||
"jetbrains/phpstorm-stubs": "dev-master",
|
||||
"composer/composer": "^1.3",
|
||||
"Microsoft/tolerant-php-parser": "^0.0.2"
|
||||
"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",
|
||||
"phan/phan": "1.1.4",
|
||||
"squizlabs/php_codesniffer": "^3.1"
|
||||
},
|
||||
"replace": {
|
||||
"felixfbecker/language-server": "self.version"
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"LanguageServer\\": "src/"
|
||||
|
@ -49,8 +50,7 @@
|
|||
"files" : [
|
||||
"src/utils.php",
|
||||
"src/FqnUtilities.php",
|
||||
"src/ParserHelpers.php",
|
||||
"vendor/squizlabs/php_codesniffer/autoload.php"
|
||||
"src/ParserHelpers.php"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
|
@ -58,8 +58,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,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,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,10 @@
|
|||
<?php
|
||||
|
||||
namespace Whatever;
|
||||
|
||||
use TestNamespace\InnerNamespace as AliasNamespace;
|
||||
|
||||
class IDontShowUpInCompletion {}
|
||||
|
||||
AliasNamespace\I;
|
||||
AliasNamespace\;
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace TestNamespace;
|
||||
|
||||
use SomeNamespace\Goo;
|
||||
|
||||
class TestClass
|
||||
{
|
||||
public $testProperty;
|
||||
|
||||
public function testMethod($testParameter)
|
||||
{
|
||||
$testVariable = 123;
|
||||
|
||||
if (empty($testParameter)) {
|
||||
echo 'Empty';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
namespace RecursiveTest;
|
||||
|
||||
class A extends A {}
|
||||
|
||||
class B extends C {}
|
||||
class C extends B {}
|
||||
|
||||
class D extends E {}
|
||||
class E extends F {}
|
||||
class F extends D {}
|
||||
|
||||
$a = new A;
|
||||
$a->undef_prop = 1;
|
||||
|
||||
$b = new B;
|
||||
$b->undef_prop = 1;
|
||||
|
||||
$d = new D;
|
||||
$d->undef_prop = 1;
|
|
@ -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();
|
|
@ -103,3 +103,8 @@ class Example {
|
|||
public function __construct() {}
|
||||
public function __destruct() {}
|
||||
}
|
||||
|
||||
namespace TestNamespace\InnerNamespace;
|
||||
|
||||
class InnerClass {
|
||||
}
|
||||
|
|
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": "docker build -t felixfbecker/php-language-server ."
|
||||
}
|
||||
],
|
||||
"publish": [
|
||||
"@semantic-release/github",
|
||||
{
|
||||
"path": "semantic-release-docker",
|
||||
"name": "felixfbecker/php-language-server"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,14 +1,27 @@
|
|||
<?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>
|
||||
|
|
|
@ -11,6 +11,11 @@ use Sabre\Event\Promise;
|
|||
*/
|
||||
class ClientCache implements Cache
|
||||
{
|
||||
/**
|
||||
* @var LanguageClient
|
||||
*/
|
||||
public $client;
|
||||
|
||||
/**
|
||||
* @param LanguageClient $client
|
||||
*/
|
||||
|
|
|
@ -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 LanguageServerProtocol\{Diagnostic, 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,7 @@ declare(strict_types = 1);
|
|||
namespace LanguageServer\Client;
|
||||
|
||||
use LanguageServer\ClientHandler;
|
||||
use LanguageServer\Protocol\TextDocumentIdentifier;
|
||||
use LanguageServerProtocol\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;
|
||||
|
||||
/**
|
||||
|
|
|
@ -41,12 +41,12 @@ class ClientHandler
|
|||
{
|
||||
$id = $this->idGenerator->generate();
|
||||
return $this->protocolWriter->write(
|
||||
new Protocol\Message(
|
||||
new Message(
|
||||
new AdvancedJsonRpc\Request($id, $method, (object)$params)
|
||||
)
|
||||
)->then(function () use ($id) {
|
||||
$promise = new Promise;
|
||||
$listener = function (Protocol\Message $msg) use ($id, $promise, &$listener) {
|
||||
$listener = function (Message $msg) use ($id, $promise, &$listener) {
|
||||
if (AdvancedJsonRpc\Response::isResponse($msg->body) && $msg->body->id === $id) {
|
||||
// Received a response
|
||||
$this->protocolReader->removeListener('message', $listener);
|
||||
|
@ -71,9 +71,8 @@ class ClientHandler
|
|||
*/
|
||||
public function notify(string $method, $params): Promise
|
||||
{
|
||||
$id = $this->idGenerator->generate();
|
||||
return $this->protocolWriter->write(
|
||||
new Protocol\Message(
|
||||
new Message(
|
||||
new AdvancedJsonRpc\Notification($method, (object)$params)
|
||||
)
|
||||
);
|
||||
|
|
|
@ -4,16 +4,28 @@ declare(strict_types = 1);
|
|||
namespace LanguageServer;
|
||||
|
||||
use LanguageServer\Index\ReadableIndex;
|
||||
use LanguageServer\Protocol\{
|
||||
use LanguageServer\Factory\CompletionItemFactory;
|
||||
use LanguageServerProtocol\{
|
||||
TextEdit,
|
||||
Range,
|
||||
Position,
|
||||
CompletionList,
|
||||
CompletionItem,
|
||||
CompletionItemKind
|
||||
CompletionItemKind,
|
||||
CompletionContext,
|
||||
CompletionTriggerKind
|
||||
};
|
||||
use Microsoft\PhpParser;
|
||||
use Microsoft\PhpParser\Node;
|
||||
use Microsoft\PhpParser\ResolvedName;
|
||||
use Generator;
|
||||
use function LanguageServer\FqnUtilities\{
|
||||
nameConcat,
|
||||
nameGetFirstPart,
|
||||
nameGetParent,
|
||||
nameStartsWith,
|
||||
nameWithoutFirstPart
|
||||
};
|
||||
|
||||
class CompletionProvider
|
||||
{
|
||||
|
@ -88,7 +100,23 @@ class CompletionProvider
|
|||
'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')
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -121,10 +149,14 @@ 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);
|
||||
|
||||
|
@ -151,7 +183,23 @@ class CompletionProvider
|
|||
|
||||
// Inspect the type of expression under the cursor
|
||||
|
||||
if ($node === null || $node instanceof Node\Statement\InlineHtml || $pos == new Position(0, 0)) {
|
||||
$content = $doc->getContent();
|
||||
$offset = $pos->toOffset($content);
|
||||
if (
|
||||
$node === null
|
||||
|| (
|
||||
$node instanceof Node\Statement\InlineHtml
|
||||
&& (
|
||||
$context !== null
|
||||
// Make sure to not suggest on the > trigger character in HTML
|
||||
&& (
|
||||
$context->triggerKind === CompletionTriggerKind::INVOKED
|
||||
|| $context->triggerCharacter === '<'
|
||||
)
|
||||
)
|
||||
)
|
||||
|| $pos == new Position(0, 0)
|
||||
) {
|
||||
// HTML, beginning of file
|
||||
|
||||
// Inside HTML and at the beginning of the file, propose <?php
|
||||
|
@ -196,24 +244,19 @@ class CompletionProvider
|
|||
// $a->|
|
||||
|
||||
// Multiple prefixes for all possible types
|
||||
$prefixes = FqnUtilities\getFqnsFromType(
|
||||
$fqns = FqnUtilities\getFqnsFromType(
|
||||
$this->definitionResolver->resolveExpressionNodeToType($node->dereferencableExpression)
|
||||
);
|
||||
|
||||
// Include parent classes
|
||||
$prefixes = $this->expandParentFqns($prefixes);
|
||||
|
||||
// Add the object access operator to only get members
|
||||
foreach ($prefixes as &$prefix) {
|
||||
$prefix .= '->';
|
||||
}
|
||||
unset($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->isGlobal) {
|
||||
$list->items[] = CompletionItem::fromDefinition($def);
|
||||
// The FQNs of the symbol and its parents (eg the implemented interfaces)
|
||||
foreach ($this->expandParentFqns($fqns) as $parentFqn) {
|
||||
// Add the object access operator to only get members of all parents
|
||||
$prefix = $parentFqn . '->';
|
||||
$prefixLen = strlen($prefix);
|
||||
// Collect fqn definitions
|
||||
foreach ($this->index->getChildDefinitionsForFqn($parentFqn) as $fqn => $def) {
|
||||
if (substr($fqn, 0, $prefixLen) === $prefix && $def->isMember) {
|
||||
$list->items[] = CompletionItemFactory::fromDefinition($def);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -232,24 +275,19 @@ class CompletionProvider
|
|||
// TODO: $a::|
|
||||
|
||||
// Resolve all possible types to FQNs
|
||||
$prefixes = FqnUtilities\getFqnsFromType(
|
||||
$fqns = FqnUtilities\getFqnsFromType(
|
||||
$classType = $this->definitionResolver->resolveExpressionNodeToType($scoped->scopeResolutionQualifier)
|
||||
);
|
||||
|
||||
// Add parent classes
|
||||
$prefixes = $this->expandParentFqns($prefixes);
|
||||
|
||||
// Append :: operator to only get static members
|
||||
foreach ($prefixes as &$prefix) {
|
||||
$prefix .= '::';
|
||||
}
|
||||
unset($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->isGlobal) {
|
||||
$list->items[] = CompletionItem::fromDefinition($def);
|
||||
// The FQNs of the symbol and its parents (eg the implemented interfaces)
|
||||
foreach ($this->expandParentFqns($fqns) as $parentFqn) {
|
||||
// Append :: operator to only get static members of all parents
|
||||
$prefix = strtolower($parentFqn . '::');
|
||||
$prefixLen = strlen($prefix);
|
||||
// Collect fqn definitions
|
||||
foreach ($this->index->getChildDefinitionsForFqn($parentFqn) as $fqn => $def) {
|
||||
if (substr(strtolower($fqn), 0, $prefixLen) === $prefix && $def->isMember) {
|
||||
$list->items[] = CompletionItemFactory::fromDefinition($def);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -266,134 +304,297 @@ class CompletionProvider
|
|||
// my_func|
|
||||
// MY_CONS|
|
||||
// MyCla|
|
||||
// \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);
|
||||
if ($nameNode instanceof Node\QualifiedName) {
|
||||
/** @var string The typed name. */
|
||||
$prefix = (string)PhpParser\ResolvedName::buildName($nameNode->nameParts, $nameNode->getFileContents());
|
||||
} else {
|
||||
$prefix = $nameNode->getText($node->getFileContents());
|
||||
}
|
||||
|
||||
/** 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 current namespace without a leading backslash. */
|
||||
$currentNamespace = $namespaceNode === null ? '' : $namespaceNode->name->getText();
|
||||
|
||||
/** @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);
|
||||
/** @var bool Whether the prefix is qualified (contains at least one backslash) */
|
||||
$isFullyQualified = false;
|
||||
|
||||
/** @var bool Whether the prefix is qualified (contains at least one backslash) */
|
||||
$isQualified = false;
|
||||
|
||||
if ($nameNode instanceof Node\QualifiedName) {
|
||||
$isFullyQualified = $nameNode->isFullyQualifiedName();
|
||||
$isQualified = $nameNode->isQualifiedName();
|
||||
}
|
||||
|
||||
// Get the namespace use statements
|
||||
// TODO: use function statements, use const statements
|
||||
/** @var bool Whether we are in a new expression */
|
||||
$isCreation = isset($creation);
|
||||
|
||||
/** @var string[] $aliases A map from local alias to fully qualified name */
|
||||
list($aliases,,) = $node->getImportTablesForCurrentScope();
|
||||
/** @var array Import (use) tables */
|
||||
$importTables = $node->getImportTablesForCurrentScope();
|
||||
|
||||
foreach ($aliases as $alias => $name) {
|
||||
$aliases[$alias] = (string)$name;
|
||||
if ($isFullyQualified) {
|
||||
// \Prefix\Goes\Here| - Only return completions from the root namespace.
|
||||
/** @var $items \Generator|CompletionItem[] Generator yielding CompletionItems indexed by their FQN */
|
||||
$items = $this->getCompletionsForFqnPrefix($prefix, $isCreation, false);
|
||||
} else if ($isQualified) {
|
||||
// Prefix\Goes\Here|
|
||||
$items = $this->getPartiallyQualifiedCompletions(
|
||||
$prefix,
|
||||
$currentNamespace,
|
||||
$importTables,
|
||||
$isCreation
|
||||
);
|
||||
} else {
|
||||
// PrefixGoesHere|
|
||||
$items = $this->getUnqualifiedCompletions($prefix, $currentNamespace, $importTables, $isCreation);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
$list->items = array_values(iterator_to_array($items));
|
||||
foreach ($list->items as $item) {
|
||||
// Remove ()
|
||||
if (is_string($item->insertText) && substr($item->insertText, strlen($item->insertText) - 2) === '()') {
|
||||
$item->insertText = substr($item->insertText, 0, -2);
|
||||
}
|
||||
}
|
||||
|
||||
// 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->isGlobal
|
||||
&& (
|
||||
!$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;
|
||||
}
|
||||
|
||||
private function getPartiallyQualifiedCompletions(
|
||||
string $prefix,
|
||||
string $currentNamespace,
|
||||
array $importTables,
|
||||
bool $requireCanBeInstantiated
|
||||
): \Generator {
|
||||
// If the first part of the partially qualified name matches a namespace alias,
|
||||
// only definitions below that alias can be completed.
|
||||
list($namespaceAliases,,) = $importTables;
|
||||
$prefixFirstPart = nameGetFirstPart($prefix);
|
||||
$foundAlias = $foundAliasFqn = null;
|
||||
foreach ($namespaceAliases as $alias => $aliasFqn) {
|
||||
if (strcasecmp($prefixFirstPart, $alias) === 0) {
|
||||
$foundAlias = $alias;
|
||||
$foundAliasFqn = (string)$aliasFqn;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($foundAlias !== null) {
|
||||
yield from $this->getCompletionsFromAliasedNamespace(
|
||||
$prefix,
|
||||
$foundAlias,
|
||||
$foundAliasFqn,
|
||||
$requireCanBeInstantiated
|
||||
);
|
||||
} else {
|
||||
yield from $this->getCompletionsForFqnPrefix(
|
||||
nameConcat($currentNamespace, $prefix),
|
||||
$requireCanBeInstantiated,
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the FQNs of all parent classes to an array of FQNs of classes
|
||||
* Yields completions for non-qualified global names.
|
||||
*
|
||||
* Yields
|
||||
* - Aliased classes
|
||||
* - Completions from current namespace
|
||||
* - Roamed completions from the global namespace (when not creating and not already in root NS)
|
||||
* - PHP keywords (when not creating)
|
||||
*
|
||||
* @return \Generator|CompletionItem[]
|
||||
* Yields CompletionItems
|
||||
*/
|
||||
private function getUnqualifiedCompletions(
|
||||
string $prefix,
|
||||
string $currentNamespace,
|
||||
array $importTables,
|
||||
bool $requireCanBeInstantiated
|
||||
): \Generator {
|
||||
// Aliases
|
||||
list($namespaceAliases,,) = $importTables;
|
||||
// use Foo\Bar
|
||||
yield from $this->getCompletionsForAliases(
|
||||
$prefix,
|
||||
$namespaceAliases,
|
||||
$requireCanBeInstantiated
|
||||
);
|
||||
|
||||
// Completions from the current namespace
|
||||
yield from $this->getCompletionsForFqnPrefix(
|
||||
nameConcat($currentNamespace, $prefix),
|
||||
$requireCanBeInstantiated,
|
||||
false
|
||||
);
|
||||
|
||||
if ($currentNamespace !== '' && $prefix === '') {
|
||||
// Get additional suggestions from the global namespace.
|
||||
// When completing e.g. for new |, suggest \DateTime
|
||||
yield from $this->getCompletionsForFqnPrefix('', $requireCanBeInstantiated, true);
|
||||
}
|
||||
|
||||
if (!$requireCanBeInstantiated) {
|
||||
if ($currentNamespace !== '' && $prefix !== '') {
|
||||
// Roamed definitions (i.e. global constants and functions). The prefix is checked against '', since
|
||||
// in that case global completions have already been provided (including non-roamed definitions.)
|
||||
yield from $this->getRoamedCompletions($prefix);
|
||||
}
|
||||
|
||||
// Lastly and least importantly, suggest keywords.
|
||||
yield from $this->getCompletionsForKeywords($prefix);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets completions for prefixes of fully qualified names in their parent namespace.
|
||||
*
|
||||
* @param string $prefix Prefix to complete for. Fully qualified.
|
||||
* @param bool $requireCanBeInstantiated If set, only return classes.
|
||||
* @param bool $insertFullyQualified If set, return completion with the leading \ inserted.
|
||||
* @return \Generator|CompletionItem[]
|
||||
* Yields CompletionItems.
|
||||
*/
|
||||
private function getCompletionsForFqnPrefix(
|
||||
string $prefix,
|
||||
bool $requireCanBeInstantiated,
|
||||
bool $insertFullyQualified
|
||||
): \Generator {
|
||||
$namespace = nameGetParent($prefix);
|
||||
foreach ($this->index->getChildDefinitionsForFqn($namespace) as $fqn => $def) {
|
||||
if ($requireCanBeInstantiated && !$def->canBeInstantiated) {
|
||||
continue;
|
||||
}
|
||||
if (!nameStartsWith($fqn, $prefix)) {
|
||||
continue;
|
||||
}
|
||||
$completion = CompletionItemFactory::fromDefinition($def);
|
||||
if ($insertFullyQualified) {
|
||||
$completion->insertText = '\\' . $fqn;
|
||||
}
|
||||
yield $fqn => $completion;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets completions for non-qualified names matching the start of an used class, function, or constant.
|
||||
*
|
||||
* @param string $prefix Non-qualified name being completed for
|
||||
* @param QualifiedName[] $aliases Array of alias FQNs indexed by the alias.
|
||||
* @return \Generator|CompletionItem[]
|
||||
* Yields CompletionItems.
|
||||
*/
|
||||
private function getCompletionsForAliases(
|
||||
string $prefix,
|
||||
array $aliases,
|
||||
bool $requireCanBeInstantiated
|
||||
): \Generator {
|
||||
foreach ($aliases as $alias => $aliasFqn) {
|
||||
if (!nameStartsWith($alias, $prefix)) {
|
||||
continue;
|
||||
}
|
||||
$definition = $this->index->getDefinition((string)$aliasFqn);
|
||||
if ($definition) {
|
||||
if ($requireCanBeInstantiated && !$definition->canBeInstantiated) {
|
||||
continue;
|
||||
}
|
||||
$completionItem = CompletionItemFactory::fromDefinition($definition);
|
||||
$completionItem->insertText = $alias;
|
||||
yield (string)$aliasFqn => $completionItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets completions for partially qualified names, where the first part is matched by an alias.
|
||||
*
|
||||
* @return \Generator|CompletionItem[]
|
||||
* Yields CompletionItems.
|
||||
*/
|
||||
private function getCompletionsFromAliasedNamespace(
|
||||
string $prefix,
|
||||
string $alias,
|
||||
string $aliasFqn,
|
||||
bool $requireCanBeInstantiated
|
||||
): \Generator {
|
||||
$prefixFirstPart = nameGetFirstPart($prefix);
|
||||
// Matched alias.
|
||||
$resolvedPrefix = nameConcat($aliasFqn, nameWithoutFirstPart($prefix));
|
||||
$completionItems = $this->getCompletionsForFqnPrefix(
|
||||
$resolvedPrefix,
|
||||
$requireCanBeInstantiated,
|
||||
false
|
||||
);
|
||||
// Convert FQNs in the CompletionItems so they are expressed in terms of the alias.
|
||||
foreach ($completionItems as $fqn => $completionItem) {
|
||||
/** @var string $fqn with the leading parts determined by the alias removed. Has the leading backslash. */
|
||||
$nameWithoutAliasedPart = substr($fqn, strlen($aliasFqn));
|
||||
$completionItem->insertText = $alias . $nameWithoutAliasedPart;
|
||||
yield $fqn => $completionItem;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets completions for globally defined functions and constants (i.e. symbols which may be used anywhere)
|
||||
*
|
||||
* @return \Generator|CompletionItem[]
|
||||
* Yields CompletionItems.
|
||||
*/
|
||||
private function getRoamedCompletions(string $prefix): \Generator
|
||||
{
|
||||
foreach ($this->index->getChildDefinitionsForFqn('') as $fqn => $def) {
|
||||
if (!$def->roamed || !nameStartsWith($fqn, $prefix)) {
|
||||
continue;
|
||||
}
|
||||
$completionItem = CompletionItemFactory::fromDefinition($def);
|
||||
// Second-guessing the user here - do not trust roaming to work. If the same symbol is
|
||||
// inserted in the current namespace, the code will stop working.
|
||||
$completionItem->insertText = '\\' . $fqn;
|
||||
yield $fqn => $completionItem;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes PHP keywords.
|
||||
*
|
||||
* @return \Generator|CompletionItem[]
|
||||
* Yields CompletionItems.
|
||||
*/
|
||||
private function getCompletionsForKeywords(string $prefix): \Generator
|
||||
{
|
||||
foreach (self::KEYWORDS as $keyword) {
|
||||
if (nameStartsWith($keyword, $prefix)) {
|
||||
$item = new CompletionItem($keyword, CompletionItemKind::KEYWORD);
|
||||
$item->insertText = $keyword;
|
||||
yield $keyword => $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -421,7 +622,7 @@ class CompletionProvider
|
|||
|
||||
// Walk the AST upwards until a scope boundary is met
|
||||
$level = $node;
|
||||
while ($level && !ParserHelpers\isFunctionLike($level)) {
|
||||
while ($level && !($level instanceof PhpParser\FunctionLike)) {
|
||||
// Walk siblings before the node
|
||||
$sibling = $level;
|
||||
while ($sibling = $sibling->getPreviousSibling()) {
|
||||
|
@ -435,7 +636,7 @@ class CompletionProvider
|
|||
|
||||
// If the traversal ended because a function was met,
|
||||
// also add its parameters and closure uses to the result list
|
||||
if ($level && ParserHelpers\isFunctionLike($level) && $level->parameters !== null) {
|
||||
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) {
|
||||
|
@ -443,8 +644,9 @@ class CompletionProvider
|
|||
}
|
||||
}
|
||||
|
||||
if ($level instanceof Node\Expression\AnonymousFunctionCreationExpression && $level->anonymousFunctionUseClause !== null &&
|
||||
$level->anonymousFunctionUseClause->useVariableNameList !== null) {
|
||||
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) {
|
||||
|
@ -475,6 +677,14 @@ class CompletionProvider
|
|||
|
||||
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[] = $descendantNode;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Get all descendent variables, then filter to ones that start with $namePrefix.
|
||||
// Avoiding closure usage in tight loop
|
||||
|
|
|
@ -56,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();
|
||||
|
|
|
@ -4,7 +4,7 @@ declare(strict_types = 1);
|
|||
namespace LanguageServer\ContentRetriever;
|
||||
|
||||
use LanguageServer\LanguageClient;
|
||||
use LanguageServer\Protocol\{TextDocumentIdentifier, TextDocumentItem};
|
||||
use LanguageServerProtocol\{TextDocumentIdentifier, TextDocumentItem};
|
||||
use Sabre\Event\Promise;
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,9 +3,10 @@ declare(strict_types = 1);
|
|||
|
||||
namespace LanguageServer;
|
||||
|
||||
use phpDocumentor\Reflection\{Types, Type, Fqsen, TypeResolver};
|
||||
use LanguageServer\Protocol\SymbolInformation;
|
||||
use Exception;
|
||||
use LanguageServer\Index\ReadableIndex;
|
||||
use phpDocumentor\Reflection\{Types, Type, TypeResolver};
|
||||
use LanguageServerProtocol\SymbolInformation;
|
||||
use Generator;
|
||||
|
||||
/**
|
||||
* Class used to represent symbols
|
||||
|
@ -37,12 +38,13 @@ 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)
|
||||
|
@ -66,7 +68,7 @@ class Definition
|
|||
public $canBeInstantiated;
|
||||
|
||||
/**
|
||||
* @var Protocol\SymbolInformation
|
||||
* @var SymbolInformation
|
||||
*/
|
||||
public $symbolInformation;
|
||||
|
||||
|
@ -76,9 +78,9 @@ 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
|
||||
* @var Type|null
|
||||
*/
|
||||
public $type;
|
||||
|
||||
|
@ -95,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,9 +4,11 @@ declare(strict_types = 1);
|
|||
namespace LanguageServer;
|
||||
|
||||
use LanguageServer\Index\ReadableIndex;
|
||||
use LanguageServer\Protocol\SymbolInformation;
|
||||
use LanguageServer\Factory\SymbolInformationFactory;
|
||||
use LanguageServerProtocol\SymbolInformation;
|
||||
use Microsoft\PhpParser;
|
||||
use Microsoft\PhpParser\Node;
|
||||
use Microsoft\PhpParser\FunctionLike;
|
||||
use phpDocumentor\Reflection\{
|
||||
DocBlock, DocBlockFactory, Fqsen, Type, TypeResolver, Types
|
||||
};
|
||||
|
@ -34,6 +36,13 @@ class DefinitionResolver
|
|||
*/
|
||||
private $docBlockFactory;
|
||||
|
||||
/**
|
||||
* Creates SignatureInformation instances
|
||||
*
|
||||
* @var SignatureInformationFactory
|
||||
*/
|
||||
private $signatureInformationFactory;
|
||||
|
||||
/**
|
||||
* @param ReadableIndex $index
|
||||
*/
|
||||
|
@ -42,6 +51,7 @@ class DefinitionResolver
|
|||
$this->index = $index;
|
||||
$this->typeResolver = new TypeResolver;
|
||||
$this->docBlockFactory = DocBlockFactory::createInstance();
|
||||
$this->signatureInformationFactory = new SignatureInformationFactory($this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -181,10 +191,8 @@ class DefinitionResolver
|
|||
);
|
||||
|
||||
// Interfaces, classes, traits, namespaces, functions, and global const elements
|
||||
$def->isGlobal = (
|
||||
$node instanceof Node\Statement\InterfaceDeclaration ||
|
||||
$node instanceof Node\Statement\ClassDeclaration ||
|
||||
$node instanceof Node\Statement\TraitDeclaration ||
|
||||
$def->isMember = !(
|
||||
$node instanceof PhpParser\ClassLike ||
|
||||
|
||||
($node instanceof Node\Statement\NamespaceDefinition && $node->name !== null) ||
|
||||
|
||||
|
@ -226,7 +234,7 @@ class DefinitionResolver
|
|||
}
|
||||
}
|
||||
|
||||
$def->symbolInformation = SymbolInformation::fromNode($node, $fqn);
|
||||
$def->symbolInformation = SymbolInformationFactory::fromNode($node, $fqn);
|
||||
|
||||
if ($def->symbolInformation !== null) {
|
||||
$def->type = $this->getTypeFromNode($node);
|
||||
|
@ -234,6 +242,10 @@ class DefinitionResolver
|
|||
$def->documentation = $this->getDocumentationFromNode($node);
|
||||
}
|
||||
|
||||
if ($node instanceof FunctionLike) {
|
||||
$def->signatureInformation = $this->signatureInformationFactory->create($node);
|
||||
}
|
||||
|
||||
return $def;
|
||||
}
|
||||
|
||||
|
@ -266,13 +278,38 @@ class DefinitionResolver
|
|||
// Other references are references to a global symbol that have an FQN
|
||||
// Find out the FQN
|
||||
$fqn = $this->resolveReferenceNodeToFqn($node);
|
||||
if ($fqn === null) {
|
||||
if (!$fqn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// If the node is a function or constant, it could be namespaced, but PHP falls back to global
|
||||
// http://php.net/manual/en/language.namespaces.fallback.php
|
||||
// TODO - verify that this is not a method
|
||||
$globalFallback = ParserHelpers\isConstantFetch($node) || $parent instanceof Node\Expression\CallExpression;
|
||||
|
||||
// Return the Definition object from the index index
|
||||
return $this->index->getDefinition($fqn, $globalFallback);
|
||||
}
|
||||
|
@ -280,6 +317,7 @@ class DefinitionResolver
|
|||
/**
|
||||
* Given any node, returns the FQN of the symbol that is referenced
|
||||
* Returns null if the FQN could not be resolved or the reference node references a variable
|
||||
* May also return "static", "self" or "parent"
|
||||
*
|
||||
* @param Node $node
|
||||
* @return string|null
|
||||
|
@ -400,6 +438,7 @@ class DefinitionResolver
|
|||
|
||||
// Find the right class that implements the member
|
||||
$implementorFqns = [$classFqn];
|
||||
$visitedFqns = [];
|
||||
|
||||
while ($implementorFqn = array_shift($implementorFqns)) {
|
||||
// If the member FQN exists, return it
|
||||
|
@ -412,10 +451,15 @@ class DefinitionResolver
|
|||
if ($implementorDef === null) {
|
||||
break;
|
||||
}
|
||||
// Note the FQN as visited
|
||||
$visitedFqns[] = $implementorFqn;
|
||||
// Repeat for parent class
|
||||
if ($implementorDef->extends) {
|
||||
foreach ($implementorDef->extends as $extends) {
|
||||
$implementorFqns[] = $extends;
|
||||
// Don't add the parent FQN if it's already been visited
|
||||
if (!\in_array($extends, $visitedFqns)) {
|
||||
$implementorFqns[] = $extends;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -489,6 +533,20 @@ class DefinitionResolver
|
|||
return (string)$classNode->getNamespacedName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type of the class a node is contained in
|
||||
* Returns null if the class is anonymous or the node is not contained in a class
|
||||
*
|
||||
* @param Node $node The node used to find the containing class
|
||||
*
|
||||
* @return Types\Object_|null
|
||||
*/
|
||||
private function getContainingClassType(Node $node)
|
||||
{
|
||||
$classFqn = $this->getContainingClassFqn($node);
|
||||
return $classFqn ? new Types\Object_(new Fqsen('\\' . $classFqn)) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the assignment or parameter node where a variable was defined
|
||||
*
|
||||
|
@ -508,10 +566,19 @@ class DefinitionResolver
|
|||
} else {
|
||||
throw new \InvalidArgumentException('$var must be Variable, Param or ClosureUse, not ' . get_class($var));
|
||||
}
|
||||
if (empty($name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$shouldDescend = function ($nodeToDescand) {
|
||||
// Make sure not to decend into functions or classes (they represent a scope boundary)
|
||||
return !($nodeToDescand instanceof PhpParser\FunctionLike || $nodeToDescand instanceof PhpParser\ClassLike);
|
||||
};
|
||||
|
||||
// Traverse the AST up
|
||||
do {
|
||||
// If a function is met, check the parameters and use statements
|
||||
if (ParserHelpers\isFunctionLike($n)) {
|
||||
if ($n instanceof PhpParser\FunctionLike) {
|
||||
if ($n->parameters !== null) {
|
||||
foreach ($n->parameters->getElements() as $param) {
|
||||
if ($param->getName() === $name) {
|
||||
|
@ -531,27 +598,59 @@ class DefinitionResolver
|
|||
}
|
||||
break;
|
||||
}
|
||||
// Check each previous sibling node for a variable assignment to that variable
|
||||
|
||||
// Check each previous sibling node and their descendents for a variable assignment to that variable
|
||||
// Each previous sibling could contain a declaration of the variable
|
||||
while (($prevSibling = $n->getPreviousSibling()) !== null && $n = $prevSibling) {
|
||||
if ($n instanceof Node\Statement\ExpressionStatement) {
|
||||
$n = $n->expression;
|
||||
}
|
||||
if (
|
||||
// TODO - clean this up
|
||||
($n instanceof Node\Expression\AssignmentExpression && $n->operator->kind === PhpParser\TokenKind::EqualsToken)
|
||||
&& $n->leftOperand instanceof Node\Expression\Variable && $n->leftOperand->getName() === $name
|
||||
) {
|
||||
|
||||
// Check the sibling itself
|
||||
if (self::isVariableDeclaration($n, $name)) {
|
||||
return $n;
|
||||
}
|
||||
|
||||
// Check descendant of this sibling (e.g. the children of a previous if block)
|
||||
foreach ($n->getDescendantNodes($shouldDescend) as $descendant) {
|
||||
if (self::isVariableDeclaration($descendant, $name)) {
|
||||
return $descendant;
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (isset($n) && $n = $n->parent);
|
||||
// Return null if nothing was found
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given Node declares the given variable name
|
||||
*
|
||||
* @param Node $n The Node to check
|
||||
* @param string $name The name of the wanted variable
|
||||
* @return bool
|
||||
*/
|
||||
private static function isVariableDeclaration(Node $n, string $name)
|
||||
{
|
||||
if (
|
||||
// TODO - clean this up
|
||||
($n instanceof Node\Expression\AssignmentExpression && $n->operator->kind === PhpParser\TokenKind::EqualsToken)
|
||||
&& $n->leftOperand instanceof Node\Expression\Variable && $n->leftOperand->getName() === $name
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
($n instanceof Node\ForeachValue || $n instanceof Node\ForeachKey)
|
||||
&& $n->expression instanceof Node\Expression\Variable
|
||||
&& $n->expression->getName() === $name
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an expression node, resolves that expression recursively to a type.
|
||||
* If the type could not be resolved, returns Types\Mixed.
|
||||
* If the type could not be resolved, returns Types\Mixed_.
|
||||
*
|
||||
* @param Node\Expression $expr
|
||||
* @return \phpDocumentor\Reflection\Type|null
|
||||
|
@ -567,7 +666,7 @@ class DefinitionResolver
|
|||
if ($expr == null || $expr instanceof PhpParser\MissingToken || $expr instanceof PhpParser\SkippedToken) {
|
||||
// TODO some members are null or Missing/SkippedToken
|
||||
// How do we handle this more generally?
|
||||
return new Types\Mixed;
|
||||
return new Types\Mixed_;
|
||||
}
|
||||
|
||||
// VARIABLE
|
||||
|
@ -575,13 +674,16 @@ class DefinitionResolver
|
|||
// $myVariable -> type of corresponding assignment expression
|
||||
if ($expr instanceof Node\Expression\Variable || $expr instanceof Node\UseVariableName) {
|
||||
if ($expr->getName() === 'this') {
|
||||
return new Types\This;
|
||||
return new Types\Object_(new Fqsen('\\' . $this->getContainingClassFqn($expr)));
|
||||
}
|
||||
// Find variable definition (parameter or assignment expression)
|
||||
$defNode = $this->resolveVariableToNode($expr);
|
||||
if ($defNode instanceof Node\Expression\AssignmentExpression || $defNode instanceof Node\UseVariableName) {
|
||||
return $this->resolveExpressionNodeToType($defNode);
|
||||
}
|
||||
if ($defNode instanceof Node\ForeachKey || $defNode instanceof Node\ForeachValue) {
|
||||
return $this->getTypeFromNode($defNode);
|
||||
}
|
||||
if ($defNode instanceof Node\Parameter) {
|
||||
return $this->getTypeFromNode($defNode);
|
||||
}
|
||||
|
@ -597,7 +699,7 @@ class DefinitionResolver
|
|||
// Find the function definition
|
||||
if ($expr->callableExpression instanceof Node\Expression) {
|
||||
// Cannot get type for dynamic function call
|
||||
return new Types\Mixed;
|
||||
return new Types\Mixed_;
|
||||
}
|
||||
|
||||
if ($expr->callableExpression instanceof Node\QualifiedName) {
|
||||
|
@ -646,7 +748,7 @@ class DefinitionResolver
|
|||
// MEMBER ACCESS EXPRESSION
|
||||
if ($expr instanceof Node\Expression\MemberAccessExpression) {
|
||||
if ($expr->memberName instanceof Node\Expression) {
|
||||
return new Types\Mixed;
|
||||
return new Types\Mixed_;
|
||||
}
|
||||
$var = $expr->dereferencableExpression;
|
||||
|
||||
|
@ -659,20 +761,28 @@ class DefinitionResolver
|
|||
if ($t instanceof Types\This) {
|
||||
$classFqn = self::getContainingClassFqn($expr);
|
||||
if ($classFqn === null) {
|
||||
return new Types\Mixed;
|
||||
return new Types\Mixed_;
|
||||
}
|
||||
} else if (!($t instanceof Types\Object_) || $t->getFqsen() === null) {
|
||||
return new Types\Mixed;
|
||||
return new Types\Mixed_;
|
||||
} else {
|
||||
$classFqn = substr((string)$t->getFqsen(), 1);
|
||||
}
|
||||
$fqn = $classFqn . '->' . $expr->memberName->getText($expr->getFileContents());
|
||||
$add = '->' . $expr->memberName->getText($expr->getFileContents());
|
||||
if ($expr->parent instanceof Node\Expression\CallExpression) {
|
||||
$fqn .= '()';
|
||||
$add .= '()';
|
||||
}
|
||||
$def = $this->index->getDefinition($fqn);
|
||||
if ($def !== null) {
|
||||
return $def->type;
|
||||
$classDef = $this->index->getDefinition($classFqn);
|
||||
if ($classDef !== null) {
|
||||
foreach ($classDef->getAncestorDefinitions($this->index, true) as $fqn => $def) {
|
||||
$def = $this->index->getDefinition($fqn . $add);
|
||||
if ($def !== null) {
|
||||
if ($def->type instanceof Types\This || $def->type instanceof Types\Self_) {
|
||||
return new Types\Object_(new Fqsen('\\' . $classFqn));
|
||||
}
|
||||
return $def->type;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -681,7 +791,7 @@ class DefinitionResolver
|
|||
if ($expr instanceof Node\Expression\ScopedPropertyAccessExpression) {
|
||||
$classType = $this->resolveClassNameToType($expr->scopeResolutionQualifier);
|
||||
if (!($classType instanceof Types\Object_) || $classType->getFqsen() === null) {
|
||||
return new Types\Mixed;
|
||||
return new Types\Mixed_;
|
||||
}
|
||||
$fqn = substr((string)$classType->getFqsen(), 1) . '::';
|
||||
|
||||
|
@ -693,7 +803,7 @@ class DefinitionResolver
|
|||
|
||||
$def = $this->index->getDefinition($fqn);
|
||||
if ($def === null) {
|
||||
return new Types\Mixed;
|
||||
return new Types\Mixed_;
|
||||
}
|
||||
return $def->type;
|
||||
}
|
||||
|
@ -855,7 +965,7 @@ class DefinitionResolver
|
|||
$keyTypes[] = $item->elementKey ? $this->resolveExpressionNodeToType($item->elementKey) : new Types\Integer;
|
||||
}
|
||||
}
|
||||
$valueTypes = array_unique($keyTypes);
|
||||
$valueTypes = array_unique($valueTypes);
|
||||
$keyTypes = array_unique($keyTypes);
|
||||
if (empty($valueTypes)) {
|
||||
$valueType = null;
|
||||
|
@ -880,7 +990,7 @@ class DefinitionResolver
|
|||
if ($expr instanceof Node\Expression\SubscriptExpression) {
|
||||
$varType = $this->resolveExpressionNodeToType($expr->postfixExpression);
|
||||
if (!($varType instanceof Types\Array_)) {
|
||||
return new Types\Mixed;
|
||||
return new Types\Mixed_;
|
||||
}
|
||||
return $varType->getValueType();
|
||||
}
|
||||
|
@ -889,14 +999,14 @@ class DefinitionResolver
|
|||
// include, require, include_once, require_once
|
||||
if ($expr instanceof Node\Expression\ScriptInclusionExpression) {
|
||||
// TODO: resolve path to PhpDocument and find return statement
|
||||
return new Types\Mixed;
|
||||
return new Types\Mixed_;
|
||||
}
|
||||
|
||||
if ($expr instanceof Node\QualifiedName) {
|
||||
return $this->resolveClassNameToType($expr);
|
||||
}
|
||||
|
||||
return new Types\Mixed;
|
||||
return new Types\Mixed_;
|
||||
}
|
||||
|
||||
|
||||
|
@ -910,7 +1020,7 @@ class DefinitionResolver
|
|||
public function resolveClassNameToType($class): Type
|
||||
{
|
||||
if ($class instanceof Node\Expression) {
|
||||
return new Types\Mixed;
|
||||
return new Types\Mixed_;
|
||||
}
|
||||
if ($class instanceof PhpParser\Token && $class->kind === PhpParser\TokenKind::ClassKeyword) {
|
||||
// Anonymous class
|
||||
|
@ -950,7 +1060,7 @@ class DefinitionResolver
|
|||
* For classes and interfaces, this is the class type (object).
|
||||
* For variables / assignments, this is the documented type or type the assignment resolves to.
|
||||
* Can also be a compound type.
|
||||
* If it is unknown, will be Types\Mixed.
|
||||
* If it is unknown, will be Types\Mixed_.
|
||||
* Returns null if the node does not have a type.
|
||||
*
|
||||
* @param Node $node
|
||||
|
@ -1004,7 +1114,7 @@ class DefinitionResolver
|
|||
}
|
||||
$type = $defaultType;
|
||||
}
|
||||
return $type ?? new Types\Mixed;
|
||||
return $type ?? new Types\Mixed_;
|
||||
}
|
||||
|
||||
// FUNCTIONS AND METHODS
|
||||
|
@ -1012,7 +1122,7 @@ class DefinitionResolver
|
|||
// 1. doc block
|
||||
// 2. return type hint
|
||||
// 3. TODO: infer from return statements
|
||||
if (ParserHelpers\isFunctionLike($node)) {
|
||||
if ($node instanceof PhpParser\FunctionLike) {
|
||||
// Functions/methods
|
||||
$docBlock = $this->getDocBlock($node);
|
||||
if (
|
||||
|
@ -1021,18 +1131,52 @@ class DefinitionResolver
|
|||
&& $returnTags[0]->getType() !== null
|
||||
) {
|
||||
// Use @return tag
|
||||
return $returnTags[0]->getType();
|
||||
$returnType = $returnTags[0]->getType();
|
||||
if ($returnType instanceof Types\Self_) {
|
||||
$selfType = $this->getContainingClassType($node);
|
||||
if ($selfType) {
|
||||
return $selfType;
|
||||
}
|
||||
}
|
||||
return $returnType;
|
||||
}
|
||||
if ($node->returnType !== null && !($node->returnType instanceof PhpParser\MissingToken)) {
|
||||
// Use PHP7 return type hint
|
||||
if ($node->returnType instanceof PhpParser\Token) {
|
||||
// Resolve a string like "bool" to a type object
|
||||
return $this->typeResolver->resolve($node->returnType->getText($node->getFileContents()));
|
||||
} elseif ($node->returnType->getResolvedName() === 'self') {
|
||||
$selfType = $this->getContainingClassType($node);
|
||||
if ($selfType !== null) {
|
||||
return $selfType;
|
||||
}
|
||||
}
|
||||
return new Types\Object_(new Fqsen('\\' . (string)$node->returnType->getResolvedName()));
|
||||
}
|
||||
// Unknown return type
|
||||
return new Types\Mixed;
|
||||
return new Types\Mixed_;
|
||||
}
|
||||
|
||||
// FOREACH KEY/VARIABLE
|
||||
if ($node instanceof Node\ForeachKey || $node->parent instanceof Node\ForeachKey) {
|
||||
$foreach = $node->getFirstAncestor(Node\Statement\ForeachStatement::class);
|
||||
$collectionType = $this->resolveExpressionNodeToType($foreach->forEachCollectionName);
|
||||
if ($collectionType instanceof Types\Array_) {
|
||||
return $collectionType->getKeyType();
|
||||
}
|
||||
return new Types\Mixed_();
|
||||
}
|
||||
|
||||
// FOREACH VALUE/VARIABLE
|
||||
if ($node instanceof Node\ForeachValue
|
||||
|| ($node instanceof Node\Expression\Variable && $node->parent instanceof Node\ForeachValue)
|
||||
) {
|
||||
$foreach = $node->getFirstAncestor(Node\Statement\ForeachStatement::class);
|
||||
$collectionType = $this->resolveExpressionNodeToType($foreach->forEachCollectionName);
|
||||
if ($collectionType instanceof Types\Array_) {
|
||||
return $collectionType->getValueType();
|
||||
}
|
||||
return new Types\Mixed_();
|
||||
}
|
||||
|
||||
// PROPERTIES, CONSTS, CLASS CONSTS, ASSIGNMENT EXPRESSIONS
|
||||
|
@ -1069,7 +1213,7 @@ class DefinitionResolver
|
|||
// TODO: read @property tags of class
|
||||
// TODO: Try to infer the type from default value / constant value
|
||||
// Unknown
|
||||
return new Types\Mixed;
|
||||
return new Types\Mixed_;
|
||||
}
|
||||
|
||||
// The node does not have a type
|
||||
|
@ -1093,11 +1237,15 @@ class DefinitionResolver
|
|||
// interface C { } A\B\C
|
||||
// trait C { } A\B\C
|
||||
if (
|
||||
$node instanceof Node\Statement\ClassDeclaration ||
|
||||
$node instanceof Node\Statement\InterfaceDeclaration ||
|
||||
$node instanceof Node\Statement\TraitDeclaration
|
||||
$node instanceof PhpParser\ClassLike
|
||||
) {
|
||||
return (string) $node->getNamespacedName();
|
||||
$className = (string)$node->getNamespacedName();
|
||||
// An (invalid) class declaration without a name will have an empty string as name,
|
||||
// but should not define an FQN
|
||||
if ($className === '') {
|
||||
return null;
|
||||
}
|
||||
return $className;
|
||||
}
|
||||
|
||||
// INPUT OUTPUT:
|
||||
|
@ -1126,9 +1274,7 @@ class DefinitionResolver
|
|||
// Class method: use ClassName->methodName() as name
|
||||
$class = $node->getFirstAncestor(
|
||||
Node\Expression\ObjectCreationExpression::class,
|
||||
Node\Statement\ClassDeclaration::class,
|
||||
Node\Statement\InterfaceDeclaration::class,
|
||||
Node\Statement\TraitDeclaration::class
|
||||
PhpParser\ClassLike::class
|
||||
);
|
||||
if (!isset($class->name)) {
|
||||
// Ignore anonymous classes
|
||||
|
@ -1152,9 +1298,7 @@ class DefinitionResolver
|
|||
($classDeclaration =
|
||||
$node->getFirstAncestor(
|
||||
Node\Expression\ObjectCreationExpression::class,
|
||||
Node\Statement\ClassDeclaration::class,
|
||||
Node\Statement\InterfaceDeclaration::class,
|
||||
Node\Statement\TraitDeclaration::class
|
||||
PhpParser\ClassLike::class
|
||||
)
|
||||
) !== null && isset($classDeclaration->name)) {
|
||||
$name = $node->getName();
|
||||
|
@ -1182,9 +1326,7 @@ class DefinitionResolver
|
|||
// Class constant: use ClassName::CONSTANT_NAME as name
|
||||
$classDeclaration = $constDeclaration->getFirstAncestor(
|
||||
Node\Expression\ObjectCreationExpression::class,
|
||||
Node\Statement\ClassDeclaration::class,
|
||||
Node\Statement\InterfaceDeclaration::class,
|
||||
Node\Statement\TraitDeclaration::class
|
||||
PhpParser\ClassLike::class
|
||||
);
|
||||
|
||||
if (!isset($classDeclaration->name)) {
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Factory;
|
||||
|
||||
use LanguageServer\Definition;
|
||||
use LanguageServerProtocol\CompletionItem;
|
||||
use LanguageServerProtocol\CompletionItemKind;
|
||||
use LanguageServerProtocol\SymbolKind;
|
||||
|
||||
class CompletionItemFactory
|
||||
{
|
||||
/**
|
||||
* Creates a CompletionItem for a Definition
|
||||
*
|
||||
* @param Definition $def
|
||||
* @return CompletionItem|null
|
||||
*/
|
||||
public static function fromDefinition(Definition $def)
|
||||
{
|
||||
$item = new CompletionItem;
|
||||
$item->label = $def->symbolInformation->name;
|
||||
$item->kind = CompletionItemKind::fromSymbolKind($def->symbolInformation->kind);
|
||||
if ($def->type) {
|
||||
$item->detail = (string)$def->type;
|
||||
} else if ($def->symbolInformation->containerName) {
|
||||
$item->detail = $def->symbolInformation->containerName;
|
||||
}
|
||||
if ($def->documentation) {
|
||||
$item->documentation = $def->documentation;
|
||||
}
|
||||
if ($def->isStatic && $def->symbolInformation->kind === SymbolKind::PROPERTY) {
|
||||
$item->insertText = '$' . $def->symbolInformation->name;
|
||||
}
|
||||
return $item;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Factory;
|
||||
|
||||
use LanguageServerProtocol\Location;
|
||||
use LanguageServerProtocol\Position;
|
||||
use LanguageServerProtocol\Range;
|
||||
use Microsoft\PhpParser\Node;
|
||||
use Microsoft\PhpParser\PositionUtilities;
|
||||
|
||||
class LocationFactory
|
||||
{
|
||||
/**
|
||||
* Returns the location of the node
|
||||
*
|
||||
* @param Node $node
|
||||
* @return self
|
||||
*/
|
||||
public static function fromNode(Node $node): Location
|
||||
{
|
||||
$range = PositionUtilities::getRangeFromPosition(
|
||||
$node->getStart(),
|
||||
$node->getWidth(),
|
||||
$node->getFileContents()
|
||||
);
|
||||
|
||||
return new Location($node->getUri(), new Range(
|
||||
new Position($range->start->line, $range->start->character),
|
||||
new Position($range->end->line, $range->end->character)
|
||||
));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Factory;
|
||||
|
||||
use LanguageServerProtocol\Position;
|
||||
use LanguageServerProtocol\Range;
|
||||
use Microsoft\PhpParser\Node;
|
||||
use Microsoft\PhpParser\PositionUtilities;
|
||||
|
||||
class RangeFactory
|
||||
{
|
||||
/**
|
||||
* Returns the range the node spans
|
||||
*
|
||||
* @param Node $node
|
||||
* @return self
|
||||
*/
|
||||
public static function fromNode(Node $node)
|
||||
{
|
||||
$range = PositionUtilities::getRangeFromPosition(
|
||||
$node->getStart(),
|
||||
$node->getWidth(),
|
||||
$node->getFileContents()
|
||||
);
|
||||
|
||||
return new Range(
|
||||
new Position($range->start->line, $range->start->character),
|
||||
new Position($range->end->line, $range->end->character)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,45 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
namespace LanguageServer\Factory;
|
||||
|
||||
use Microsoft\PhpParser;
|
||||
use LanguageServerProtocol\Location;
|
||||
use LanguageServerProtocol\SymbolInformation;
|
||||
use LanguageServerProtocol\SymbolKind;
|
||||
use Microsoft\PhpParser\Node;
|
||||
use Exception;
|
||||
use Microsoft\PhpParser\ResolvedName;
|
||||
use LanguageServer\Factory\LocationFactory;
|
||||
|
||||
/**
|
||||
* Represents information about programming constructs like variables, classes,
|
||||
* interfaces etc.
|
||||
*/
|
||||
class SymbolInformation
|
||||
class SymbolInformationFactory
|
||||
{
|
||||
/**
|
||||
* The name of this symbol.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* The kind of this symbol.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $kind;
|
||||
|
||||
/**
|
||||
* The location of this symbol.
|
||||
*
|
||||
* @var Location
|
||||
*/
|
||||
public $location;
|
||||
|
||||
/**
|
||||
* The name of the symbol containing this symbol.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $containerName;
|
||||
|
||||
/**
|
||||
* Converts a Node to a SymbolInformation
|
||||
*
|
||||
|
@ -49,7 +20,7 @@ class SymbolInformation
|
|||
*/
|
||||
public static function fromNode($node, string $fqn = null)
|
||||
{
|
||||
$symbol = new self;
|
||||
$symbol = new SymbolInformation();
|
||||
if ($node instanceof Node\Statement\ClassDeclaration) {
|
||||
$symbol->kind = SymbolKind::CLASS_;
|
||||
} else if ($node instanceof Node\Statement\TraitDeclaration) {
|
||||
|
@ -99,7 +70,7 @@ class SymbolInformation
|
|||
$symbol->name = $node->getName();
|
||||
} else if (isset($node->name)) {
|
||||
if ($node->name instanceof Node\QualifiedName) {
|
||||
$symbol->name = (string)PhpParser\ResolvedName::buildName($node->name->nameParts, $node->getFileContents());
|
||||
$symbol->name = (string)ResolvedName::buildName($node->name->nameParts, $node->getFileContents());
|
||||
} else {
|
||||
$symbol->name = ltrim((string)$node->name->getText($node->getFileContents()), "$");
|
||||
}
|
||||
|
@ -109,7 +80,7 @@ class SymbolInformation
|
|||
return null;
|
||||
}
|
||||
|
||||
$symbol->location = Location::fromNode($node);
|
||||
$symbol->location = LocationFactory::fromNode($node);
|
||||
if ($fqn !== null) {
|
||||
$parts = preg_split('/(::|->|\\\\)/', $fqn);
|
||||
array_pop($parts);
|
||||
|
@ -117,18 +88,4 @@ class SymbolInformation
|
|||
}
|
||||
return $symbol;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param int $kind
|
||||
* @param Location $location
|
||||
* @param string $containerName
|
||||
*/
|
||||
public function __construct($name = null, $kind = null, $location = null, $containerName = null)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->kind = $kind;
|
||||
$this->location = $location;
|
||||
$this->containerName = $containerName;
|
||||
}
|
||||
}
|
|
@ -3,7 +3,6 @@ declare(strict_types = 1);
|
|||
|
||||
namespace LanguageServer\FilesFinder;
|
||||
|
||||
use Webmozart\Glob\Iterator\GlobIterator;
|
||||
use Sabre\Event\Promise;
|
||||
use function Sabre\Event\coroutine;
|
||||
use function LanguageServer\{pathToUri, timeout};
|
||||
|
@ -23,7 +22,7 @@ class FileSystemFilesFinder implements FilesFinder
|
|||
$uris = [];
|
||||
foreach (new GlobIterator($glob) as $path) {
|
||||
// Exclude any directories that also match the glob pattern
|
||||
if (!is_dir($path)) {
|
||||
if (!is_dir($path) || !is_readable($path)) {
|
||||
$uris[] = pathToUri($path);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace LanguageServer\FilesFinder;
|
||||
|
||||
use ArrayIterator;
|
||||
use EmptyIterator;
|
||||
use IteratorIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use Webmozart\Glob\Glob;
|
||||
use Webmozart\Glob\Iterator\GlobFilterIterator;
|
||||
use Webmozart\Glob\Iterator\RecursiveDirectoryIterator;
|
||||
|
||||
/**
|
||||
* Returns filesystem paths matching a glob.
|
||||
*
|
||||
* @since 1.0
|
||||
*
|
||||
* @author Bernhard Schussek <bschussek@gmail.com>
|
||||
*
|
||||
* @see Glob
|
||||
*/
|
||||
class GlobIterator extends IteratorIterator
|
||||
{
|
||||
/**
|
||||
* Creates a new iterator.
|
||||
*
|
||||
* @param string $glob The glob pattern.
|
||||
* @param int $flags A bitwise combination of the flag constants in
|
||||
* {@link Glob}.
|
||||
*/
|
||||
public function __construct($glob, $flags = 0)
|
||||
{
|
||||
$basePath = Glob::getBasePath($glob, $flags);
|
||||
if (!Glob::isDynamic($glob) && file_exists($glob)) {
|
||||
// If the glob is a file path, return that path
|
||||
$innerIterator = new ArrayIterator(array($glob));
|
||||
} elseif (is_dir($basePath)) {
|
||||
// Use the system's much more efficient glob() function where we can
|
||||
if (
|
||||
// glob() does not support /**/
|
||||
false === strpos($glob, '/**/') &&
|
||||
// glob() does not support stream wrappers
|
||||
false === strpos($glob, '://') &&
|
||||
// glob() does not support [^...] on Windows
|
||||
('\\' !== DIRECTORY_SEPARATOR || false === strpos($glob, '[^'))
|
||||
) {
|
||||
$results = glob($glob, GLOB_BRACE);
|
||||
|
||||
// $results may be empty or false if $glob is invalid
|
||||
if (empty($results)) {
|
||||
// Parse glob and provoke errors if invalid
|
||||
Glob::toRegEx($glob);
|
||||
|
||||
// Otherwise return empty result set
|
||||
$innerIterator = new EmptyIterator();
|
||||
} else {
|
||||
$innerIterator = new ArrayIterator($results);
|
||||
}
|
||||
} else {
|
||||
// Otherwise scan the glob's base directory for matches
|
||||
$innerIterator = new GlobFilterIterator(
|
||||
$glob,
|
||||
new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator(
|
||||
$basePath,
|
||||
RecursiveDirectoryIterator::CURRENT_AS_PATHNAME,
|
||||
RecursiveDirectoryIterator::SKIP_DOTS
|
||||
),
|
||||
RecursiveIteratorIterator::SELF_FIRST,
|
||||
RecursiveIteratorIterator::CATCH_GET_CHILD
|
||||
),
|
||||
GlobFilterIterator::FILTER_VALUE,
|
||||
$flags
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// If the glob's base directory does not exist, return nothing
|
||||
$innerIterator = new EmptyIterator();
|
||||
}
|
||||
|
||||
parent::__construct($innerIterator);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -3,7 +3,6 @@
|
|||
namespace LanguageServer\FqnUtilities;
|
||||
|
||||
use phpDocumentor\Reflection\{Type, Types};
|
||||
use Microsoft\PhpParser;
|
||||
|
||||
/**
|
||||
* Returns all possible FQNs in a type
|
||||
|
@ -22,10 +21,98 @@ function getFqnsFromType($type): array
|
|||
}
|
||||
if ($type instanceof Types\Compound) {
|
||||
for ($i = 0; $t = $type->get($i); $i++) {
|
||||
foreach (getFqnsFromType($type) as $fqn) {
|
||||
foreach (getFqnsFromType($t) as $fqn) {
|
||||
$fqns[] = $fqn;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $fqns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns parent of an FQN.
|
||||
*
|
||||
* getFqnParent('') === ''
|
||||
* getFqnParent('\\') === ''
|
||||
* getFqnParent('\A') === ''
|
||||
* getFqnParent('A') === ''
|
||||
* getFqnParent('\A\') === '\A' // Empty trailing name is considered a name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function nameGetParent(string $name): string
|
||||
{
|
||||
if ($name === '') { // Special-case handling for the root namespace.
|
||||
return '';
|
||||
}
|
||||
$parts = explode('\\', $name);
|
||||
array_pop($parts);
|
||||
return implode('\\', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenates two names.
|
||||
*
|
||||
* nameConcat('\Foo\Bar', 'Baz') === '\Foo\Bar\Baz'
|
||||
* nameConcat('\Foo\Bar\\', '\Baz') === '\Foo\Bar\Baz'
|
||||
* nameConcat('\\', 'Baz') === '\Baz'
|
||||
* nameConcat('', 'Baz') === 'Baz'
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function nameConcat(string $a, string $b): string
|
||||
{
|
||||
if ($a === '') {
|
||||
return $b;
|
||||
}
|
||||
$a = rtrim($a, '\\');
|
||||
$b = ltrim($b, '\\');
|
||||
return "$a\\$b";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first component of $name.
|
||||
*
|
||||
* nameGetFirstPart('Foo\Bar') === 'Foo'
|
||||
* nameGetFirstPart('\Foo\Bar') === 'Foo'
|
||||
* nameGetFirstPart('') === ''
|
||||
* nameGetFirstPart('\') === ''
|
||||
*/
|
||||
function nameGetFirstPart(string $name): string
|
||||
{
|
||||
$parts = explode('\\', $name, 3);
|
||||
if ($parts[0] === '' && count($parts) > 1) {
|
||||
return $parts[1];
|
||||
} else {
|
||||
return $parts[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the first component of $name.
|
||||
*
|
||||
* nameWithoutFirstPart('Foo\Bar') === 'Bar'
|
||||
* nameWithoutFirstPart('\Foo\Bar') === 'Bar'
|
||||
* nameWithoutFirstPart('') === ''
|
||||
* nameWithoutFirstPart('\') === ''
|
||||
*/
|
||||
function nameWithoutFirstPart(string $name): string
|
||||
{
|
||||
$parts = explode('\\', $name, 3);
|
||||
if ($parts[0] === '') {
|
||||
array_shift($parts);
|
||||
}
|
||||
array_shift($parts);
|
||||
return implode('\\', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name Name to match against
|
||||
* @param string $prefix Prefix $name has to starts with
|
||||
* @return bool
|
||||
*/
|
||||
function nameStartsWith(string $name, string $prefix): bool
|
||||
{
|
||||
return strlen($name) >= strlen($prefix)
|
||||
&& strncmp($name, $prefix, strlen($prefix)) === 0;
|
||||
}
|
||||
|
|
|
@ -99,20 +99,29 @@ abstract class AbstractAggregateIndex implements ReadableIndex
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns an associative array [string => Definition] that maps fully qualified symbol names
|
||||
* to Definitions
|
||||
* Returns a Generator providing an associative array [string => Definition]
|
||||
* that maps fully qualified symbol names to Definitions (global or not)
|
||||
*
|
||||
* @return Definition[]
|
||||
* @return \Generator yields Definition
|
||||
*/
|
||||
public function getDefinitions(): array
|
||||
public function getDefinitions(): \Generator
|
||||
{
|
||||
$defs = [];
|
||||
foreach ($this->getIndexes() as $index) {
|
||||
foreach ($index->getDefinitions() as $fqn => $def) {
|
||||
$defs[$fqn] = $def;
|
||||
}
|
||||
yield from $index->getDefinitions();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Generator that yields all the direct child Definitions of a given FQN
|
||||
*
|
||||
* @param string $fqn
|
||||
* @return \Generator yields Definition
|
||||
*/
|
||||
public function getChildDefinitionsForFqn(string $fqn): \Generator
|
||||
{
|
||||
foreach ($this->getIndexes() as $index) {
|
||||
yield from $index->getChildDefinitionsForFqn($fqn);
|
||||
}
|
||||
return $defs;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -132,19 +141,15 @@ abstract class AbstractAggregateIndex implements ReadableIndex
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns all URIs in this index that reference a symbol
|
||||
* Returns a Generator providing all URIs in this index that reference a symbol
|
||||
*
|
||||
* @param string $fqn The fully qualified name of the symbol
|
||||
* @return string[]
|
||||
* @return \Generator yields string
|
||||
*/
|
||||
public function getReferenceUris(string $fqn): array
|
||||
public function getReferenceUris(string $fqn): \Generator
|
||||
{
|
||||
$refs = [];
|
||||
foreach ($this->getIndexes() as $index) {
|
||||
foreach ($index->getReferenceUris($fqn) as $ref) {
|
||||
$refs[] = $ref;
|
||||
}
|
||||
yield from $index->getReferenceUris($fqn);
|
||||
}
|
||||
return $refs;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,14 +15,26 @@ class Index implements ReadableIndex, \Serializable
|
|||
use EmitterTrait;
|
||||
|
||||
/**
|
||||
* An associative array that maps fully qualified symbol names to Definitions
|
||||
* An associative array that maps splitted fully qualified symbol names
|
||||
* to definitions, eg :
|
||||
* [
|
||||
* 'Psr' => [
|
||||
* '\Log' => [
|
||||
* '\LoggerInterface' => [
|
||||
* '' => $def1, // definition for 'Psr\Log\LoggerInterface' which is non-member
|
||||
* '->log()' => $def2, // definition for 'Psr\Log\LoggerInterface->log()' which is a member
|
||||
* ],
|
||||
* ],
|
||||
* ],
|
||||
* ]
|
||||
*
|
||||
* @var Definition[]
|
||||
* @var array
|
||||
*/
|
||||
private $definitions = [];
|
||||
|
||||
/**
|
||||
* An associative array that maps fully qualified symbol names to arrays of document URIs that reference the symbol
|
||||
* An associative array that maps fully qualified symbol names
|
||||
* to arrays of document URIs that reference the symbol
|
||||
*
|
||||
* @var string[][]
|
||||
*/
|
||||
|
@ -84,14 +96,46 @@ class Index implements ReadableIndex, \Serializable
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns an associative array [string => Definition] that maps fully qualified symbol names
|
||||
* to Definitions
|
||||
* Returns a Generator providing an associative array [string => Definition]
|
||||
* that maps fully qualified symbol names to Definitions (global or not)
|
||||
*
|
||||
* @return Definition[]
|
||||
* @return \Generator yields Definition
|
||||
*/
|
||||
public function getDefinitions(): array
|
||||
public function getDefinitions(): \Generator
|
||||
{
|
||||
return $this->definitions;
|
||||
yield from $this->yieldDefinitionsRecursively($this->definitions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Generator that yields all the direct child Definitions of a given FQN
|
||||
*
|
||||
* @param string $fqn
|
||||
* @return \Generator yields Definition
|
||||
*/
|
||||
public function getChildDefinitionsForFqn(string $fqn): \Generator
|
||||
{
|
||||
$parts = $this->splitFqn($fqn);
|
||||
if ('' === end($parts)) {
|
||||
// we want to return all the definitions in the given FQN, not only
|
||||
// the one (non member) matching exactly the FQN.
|
||||
array_pop($parts);
|
||||
}
|
||||
|
||||
$result = $this->getIndexValue($parts, $this->definitions);
|
||||
if (!$result) {
|
||||
return;
|
||||
}
|
||||
foreach ($result as $name => $item) {
|
||||
// Don't yield the parent
|
||||
if ($name === '') {
|
||||
continue;
|
||||
}
|
||||
if ($item instanceof Definition) {
|
||||
yield $fqn.$name => $item;
|
||||
} elseif (is_array($item) && isset($item[''])) {
|
||||
yield $fqn.$name => $item[''];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -103,12 +147,17 @@ class Index implements ReadableIndex, \Serializable
|
|||
*/
|
||||
public function getDefinition(string $fqn, bool $globalFallback = false)
|
||||
{
|
||||
if (isset($this->definitions[$fqn])) {
|
||||
return $this->definitions[$fqn];
|
||||
$parts = $this->splitFqn($fqn);
|
||||
$result = $this->getIndexValue($parts, $this->definitions);
|
||||
|
||||
if ($result instanceof Definition) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($globalFallback) {
|
||||
$parts = explode('\\', $fqn);
|
||||
$fqn = end($parts);
|
||||
|
||||
return $this->getDefinition($fqn);
|
||||
}
|
||||
}
|
||||
|
@ -122,7 +171,9 @@ class Index implements ReadableIndex, \Serializable
|
|||
*/
|
||||
public function setDefinition(string $fqn, Definition $definition)
|
||||
{
|
||||
$this->definitions[$fqn] = $definition;
|
||||
$parts = $this->splitFqn($fqn);
|
||||
$this->indexDefinition(0, $parts, $this->definitions, $definition);
|
||||
|
||||
$this->emit('definition-added');
|
||||
}
|
||||
|
||||
|
@ -135,19 +186,23 @@ class Index implements ReadableIndex, \Serializable
|
|||
*/
|
||||
public function removeDefinition(string $fqn)
|
||||
{
|
||||
unset($this->definitions[$fqn]);
|
||||
$parts = $this->splitFqn($fqn);
|
||||
$this->removeIndexedDefinition(0, $parts, $this->definitions, $this->definitions);
|
||||
|
||||
unset($this->references[$fqn]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all URIs in this index that reference a symbol
|
||||
* Returns a Generator providing all URIs in this index that reference a symbol
|
||||
*
|
||||
* @param string $fqn The fully qualified name of the symbol
|
||||
* @return string[]
|
||||
* @return \Generator yields string
|
||||
*/
|
||||
public function getReferenceUris(string $fqn): array
|
||||
public function getReferenceUris(string $fqn): \Generator
|
||||
{
|
||||
return $this->references[$fqn] ?? [];
|
||||
foreach ($this->references[$fqn] ?? [] as $uri) {
|
||||
yield $uri;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -204,22 +259,184 @@ class Index implements ReadableIndex, \Serializable
|
|||
public function unserialize($serialized)
|
||||
{
|
||||
$data = unserialize($serialized);
|
||||
|
||||
if (isset($data['definitions'])) {
|
||||
foreach ($data['definitions'] as $fqn => $definition) {
|
||||
$this->setDefinition($fqn, $definition);
|
||||
}
|
||||
|
||||
unset($data['definitions']);
|
||||
}
|
||||
|
||||
foreach ($data as $prop => $val) {
|
||||
$this->$prop = $val;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $serialized
|
||||
* @return string
|
||||
*/
|
||||
public function serialize()
|
||||
{
|
||||
return serialize([
|
||||
'definitions' => $this->definitions,
|
||||
'definitions' => iterator_to_array($this->getDefinitions()),
|
||||
'references' => $this->references,
|
||||
'complete' => $this->complete,
|
||||
'staticComplete' => $this->staticComplete
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Generator that yields all the Definitions in the given $storage recursively.
|
||||
* The generator yields key => value pairs, e.g.
|
||||
* `'Psr\Log\LoggerInterface->log()' => $definition`
|
||||
*
|
||||
* @param array &$storage
|
||||
* @param string $prefix (optional)
|
||||
* @return \Generator
|
||||
*/
|
||||
private function yieldDefinitionsRecursively(array &$storage, string $prefix = ''): \Generator
|
||||
{
|
||||
foreach ($storage as $key => $value) {
|
||||
if (!is_array($value)) {
|
||||
yield $prefix.$key => $value;
|
||||
} else {
|
||||
yield from $this->yieldDefinitionsRecursively($value, $prefix.$key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits the given FQN into an array, eg :
|
||||
* - `'Psr\Log\LoggerInterface->log'` will be `['Psr', '\Log', '\LoggerInterface', '->log()']`
|
||||
* - `'\Exception->getMessage()'` will be `['\Exception', '->getMessage()']`
|
||||
* - `'PHP_VERSION'` will be `['PHP_VERSION']`
|
||||
*
|
||||
* @param string $fqn
|
||||
* @return string[]
|
||||
*/
|
||||
private function splitFqn(string $fqn): array
|
||||
{
|
||||
// split fqn at backslashes
|
||||
$parts = explode('\\', $fqn);
|
||||
|
||||
// write back the backslash prefix to the first part if it was present
|
||||
if ('' === $parts[0] && count($parts) > 1) {
|
||||
$parts = array_slice($parts, 1);
|
||||
$parts[0] = '\\' . $parts[0];
|
||||
}
|
||||
|
||||
// write back the backslashes prefixes for the other parts
|
||||
for ($i = 1; $i < count($parts); $i++) {
|
||||
$parts[$i] = '\\' . $parts[$i];
|
||||
}
|
||||
|
||||
// split the last part in 2 parts at the operator
|
||||
$hasOperator = false;
|
||||
$lastPart = end($parts);
|
||||
foreach (['::', '->'] as $operator) {
|
||||
$endParts = explode($operator, $lastPart);
|
||||
if (count($endParts) > 1) {
|
||||
$hasOperator = true;
|
||||
// replace the last part by its pieces
|
||||
array_pop($parts);
|
||||
$parts[] = $endParts[0];
|
||||
$parts[] = $operator . $endParts[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// The end($parts) === '' holds for the root namespace.
|
||||
if (!$hasOperator && end($parts) !== '') {
|
||||
// add an empty part to store the non-member definition to avoid
|
||||
// definition collisions in the index array, eg
|
||||
// 'Psr\Log\LoggerInterface' will be stored at
|
||||
// ['Psr']['\Log']['\LoggerInterface'][''] to be able to also store
|
||||
// member definitions, ie 'Psr\Log\LoggerInterface->log()' will be
|
||||
// stored at ['Psr']['\Log']['\LoggerInterface']['->log()']
|
||||
$parts[] = '';
|
||||
}
|
||||
|
||||
return $parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the values stored in this index under the given $parts array.
|
||||
* It can be an index node or a Definition if the $parts are precise
|
||||
* enough. Returns null when nothing is found.
|
||||
*
|
||||
* @param string[] $path The splitted FQN
|
||||
* @param array|Definition &$storage The current level to look for $path.
|
||||
* @return array|Definition|null
|
||||
*/
|
||||
private function getIndexValue(array $path, &$storage)
|
||||
{
|
||||
// Empty path returns the object itself.
|
||||
if (empty($path)) {
|
||||
return $storage;
|
||||
}
|
||||
|
||||
$part = array_shift($path);
|
||||
|
||||
if (!isset($storage[$part])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->getIndexValue($path, $storage[$part]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive function that stores the given Definition in the given $storage array represented
|
||||
* as a tree matching the given $parts.
|
||||
*
|
||||
* @param int $level The current level of FQN part
|
||||
* @param string[] $parts The splitted FQN
|
||||
* @param array &$storage The array in which to store the $definition
|
||||
* @param Definition $definition The Definition to store
|
||||
*/
|
||||
private function indexDefinition(int $level, array $parts, array &$storage, Definition $definition)
|
||||
{
|
||||
$part = $parts[$level];
|
||||
|
||||
if ($level + 1 === count($parts)) {
|
||||
$storage[$part] = $definition;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset($storage[$part])) {
|
||||
$storage[$part] = [];
|
||||
}
|
||||
|
||||
$this->indexDefinition($level + 1, $parts, $storage[$part], $definition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive function that removes the definition matching the given $parts from the given
|
||||
* $storage array. The function also looks up recursively to remove the parents of the
|
||||
* definition which no longer has children to avoid to let empty arrays in the index.
|
||||
*
|
||||
* @param int $level The current level of FQN part
|
||||
* @param string[] $parts The splitted FQN
|
||||
* @param array &$storage The current array in which to remove data
|
||||
* @param array &$rootStorage The root storage array
|
||||
*/
|
||||
private function removeIndexedDefinition(int $level, array $parts, array &$storage, array &$rootStorage)
|
||||
{
|
||||
$part = $parts[$level];
|
||||
|
||||
if ($level + 1 === count($parts)) {
|
||||
if (isset($storage[$part])) {
|
||||
unset($storage[$part]);
|
||||
|
||||
if (0 === count($storage)) {
|
||||
// parse again the definition tree to remove the parent
|
||||
// when it has no more children
|
||||
$this->removeIndexedDefinition(0, array_slice($parts, 0, $level), $rootStorage, $rootStorage);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->removeIndexedDefinition($level + 1, $parts, $storage[$part], $rootStorage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,12 +30,20 @@ interface ReadableIndex extends EmitterInterface
|
|||
public function isStaticComplete(): bool;
|
||||
|
||||
/**
|
||||
* Returns an associative array [string => Definition] that maps fully qualified symbol names
|
||||
* to Definitions
|
||||
* Returns a Generator providing an associative array [string => Definition]
|
||||
* that maps fully qualified symbol names to Definitions (global or not)
|
||||
*
|
||||
* @return Definitions[]
|
||||
* @return \Generator yields Definition
|
||||
*/
|
||||
public function getDefinitions(): array;
|
||||
public function getDefinitions(): \Generator;
|
||||
|
||||
/**
|
||||
* Returns a Generator that yields all the direct child Definitions of a given FQN
|
||||
*
|
||||
* @param string $fqn
|
||||
* @return \Generator yields Definition
|
||||
*/
|
||||
public function getChildDefinitionsForFqn(string $fqn): \Generator;
|
||||
|
||||
/**
|
||||
* Returns the Definition object by a specific FQN
|
||||
|
@ -47,10 +55,10 @@ interface ReadableIndex extends EmitterInterface
|
|||
public function getDefinition(string $fqn, bool $globalFallback = false);
|
||||
|
||||
/**
|
||||
* Returns all URIs in this index that reference a symbol
|
||||
* Returns a Generator that yields all URIs in this index that reference a symbol
|
||||
*
|
||||
* @param string $fqn The fully qualified name of the symbol
|
||||
* @return string[]
|
||||
* @return \Generator yields string
|
||||
*/
|
||||
public function getReferenceUris(string $fqn): array;
|
||||
public function getReferenceUris(string $fqn): \Generator;
|
||||
}
|
||||
|
|
|
@ -6,19 +6,17 @@ namespace LanguageServer;
|
|||
use LanguageServer\Cache\Cache;
|
||||
use LanguageServer\FilesFinder\FilesFinder;
|
||||
use LanguageServer\Index\{DependenciesIndex, Index};
|
||||
use LanguageServer\Protocol\Message;
|
||||
use LanguageServer\Protocol\MessageType;
|
||||
use LanguageServerProtocol\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 = 3;
|
||||
|
||||
/**
|
||||
* @var FilesFinder
|
||||
|
@ -149,7 +147,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');
|
||||
|
|
|
@ -3,14 +3,15 @@ declare(strict_types = 1);
|
|||
|
||||
namespace LanguageServer;
|
||||
|
||||
use LanguageServer\Protocol\{
|
||||
use LanguageServerProtocol\{
|
||||
ServerCapabilities,
|
||||
ClientCapabilities,
|
||||
TextDocumentSyncKind,
|
||||
Message,
|
||||
InitializeResult,
|
||||
CompletionOptions
|
||||
CompletionOptions,
|
||||
SignatureHelpOptions
|
||||
};
|
||||
use LanguageServer\Message;
|
||||
use LanguageServer\FilesFinder\{FilesFinder, ClientFilesFinder, FileSystemFilesFinder};
|
||||
use LanguageServer\ContentRetriever\{ContentRetriever, ClientContentRetriever, FileSystemContentRetriever};
|
||||
use LanguageServer\Index\{DependenciesIndex, GlobalIndex, Index, ProjectIndex, StubsIndex};
|
||||
|
@ -164,8 +165,11 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
|
|||
* @param int|null $processId The process Id of the parent process that started the server. Is null if the process has not been started by another process. If the parent process is not alive then the server should exit (see exit notification) its process.
|
||||
* @return Promise <InitializeResult>
|
||||
*/
|
||||
public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null): Promise
|
||||
public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null, string $rootUri = null): Promise
|
||||
{
|
||||
if ($rootPath === null && $rootUri !== null) {
|
||||
$rootPath = uriToPath($rootUri);
|
||||
}
|
||||
return coroutine(function () use ($capabilities, $rootPath, $processId) {
|
||||
|
||||
if ($capabilities->xfilesProvider) {
|
||||
|
@ -265,8 +269,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"
|
||||
|
@ -277,6 +279,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,9 +1,10 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
namespace LanguageServer;
|
||||
|
||||
use AdvancedJsonRpc\Message as MessageBody;
|
||||
use LanguageServer\Message;
|
||||
|
||||
class Message
|
||||
{
|
|
@ -25,7 +25,7 @@ function isConstantFetch(Node $node) : bool
|
|||
$parent instanceof Node\Expression\CallExpression ||
|
||||
$parent instanceof Node\Expression\ObjectCreationExpression ||
|
||||
$parent instanceof Node\Expression\ScopedPropertyAccessExpression ||
|
||||
isFunctionLike($parent) ||
|
||||
$parent instanceof PhpParser\FunctionLike ||
|
||||
(
|
||||
$parent instanceof Node\Expression\BinaryExpression &&
|
||||
$parent->operator->kind === PhpParser\TokenKind::InstanceOfKeyword
|
||||
|
@ -38,14 +38,6 @@ function getFunctionLikeDeclarationFromParameter(Node\Parameter $node)
|
|||
return $node->parent->parent;
|
||||
}
|
||||
|
||||
function isFunctionLike(Node $node)
|
||||
{
|
||||
return
|
||||
$node instanceof Node\Statement\FunctionDeclaration ||
|
||||
$node instanceof Node\MethodDeclaration ||
|
||||
$node instanceof Node\Expression\AnonymousFunctionCreationExpression;
|
||||
}
|
||||
|
||||
function isBooleanExpression($expression) : bool
|
||||
{
|
||||
if (!($expression instanceof Node\Expression\BinaryExpression)) {
|
||||
|
|
|
@ -4,7 +4,7 @@ declare(strict_types = 1);
|
|||
namespace LanguageServer;
|
||||
|
||||
use LanguageServer\Index\Index;
|
||||
use LanguageServer\Protocol\{
|
||||
use LanguageServerProtocol\{
|
||||
Diagnostic, Position, Range
|
||||
};
|
||||
use Microsoft\PhpParser;
|
||||
|
@ -63,7 +63,7 @@ class PhpDocument
|
|||
/**
|
||||
* Map from fully qualified name (FQN) to Node
|
||||
*
|
||||
* @var Node
|
||||
* @var Node[]
|
||||
*/
|
||||
private $definitionNodes;
|
||||
|
||||
|
@ -160,25 +160,14 @@ class PhpDocument
|
|||
|
||||
// Register this document on the project for references
|
||||
foreach ($this->referenceNodes as $fqn => $nodes) {
|
||||
$this->index->addReferenceUri($fqn, $this->uri);
|
||||
// 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->sourceFileNode = $treeAnalyzer->getSourceFileNode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns array of TextEdit changes to format this document.
|
||||
*
|
||||
* @return \LanguageServer\Protocol\TextEdit[]
|
||||
*/
|
||||
public function getFormattedText()
|
||||
{
|
||||
if (empty($this->getContent())) {
|
||||
return [];
|
||||
}
|
||||
return Formatter::format($this->getContent(), $this->uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this document's text content.
|
||||
*
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
class ClientCapabilities
|
||||
{
|
||||
/**
|
||||
* The client supports workspace/xfiles requests
|
||||
*
|
||||
* @var bool|null
|
||||
*/
|
||||
public $xfilesProvider;
|
||||
|
||||
/**
|
||||
* The client supports textDocument/xcontent requests
|
||||
*
|
||||
* @var bool|null
|
||||
*/
|
||||
public $xcontentProvider;
|
||||
|
||||
/**
|
||||
* The client supports xcache/* requests
|
||||
*
|
||||
* @var bool|null
|
||||
*/
|
||||
public $xcacheProvider;
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
/**
|
||||
* Contains additional diagnostic information about the context in which
|
||||
* a code action is run.
|
||||
*/
|
||||
class CodeActionContext
|
||||
{
|
||||
/**
|
||||
* An array of diagnostics.
|
||||
*
|
||||
* @var Diagnostic[]
|
||||
*/
|
||||
public $diagnostics;
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
/**
|
||||
* A code lens represents a command that should be shown along with
|
||||
* source text, like the number of references, a way to run tests, etc.
|
||||
*
|
||||
* A code lens is _unresolved_ when no command is associated to it. For performance
|
||||
* reasons the creation of a code lens and resolving should be done in two stages.
|
||||
*/
|
||||
class CodeLens
|
||||
{
|
||||
/**
|
||||
* The range in which this code lens is valid. Should only span a single line.
|
||||
*
|
||||
* @var Range
|
||||
*/
|
||||
public $range;
|
||||
|
||||
/**
|
||||
* The command this code lens represents.
|
||||
*
|
||||
* @var Command|null
|
||||
*/
|
||||
public $command;
|
||||
|
||||
/**
|
||||
* A data entry field that is preserved on a code lens item between
|
||||
* a code lens and a code lens resolve request.
|
||||
*
|
||||
* @var mixed|null
|
||||
*/
|
||||
public $data;
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
/**
|
||||
* Code Lens options.
|
||||
*/
|
||||
class CodeLensOptions
|
||||
{
|
||||
/**
|
||||
* Code lens has a resolve provider as well.
|
||||
*
|
||||
* @var bool|null
|
||||
*/
|
||||
public $resolveProvider;
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
/**
|
||||
* Represents a reference to a command. Provides a title which will be used to represent a command in the UI and,
|
||||
* optionally, an array of arguments which will be passed to the command handler function when invoked.
|
||||
*/
|
||||
class Command
|
||||
{
|
||||
/**
|
||||
* Title of the command, like `save`.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $title;
|
||||
|
||||
/**
|
||||
* The identifier of the actual command handler.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $command;
|
||||
|
||||
/**
|
||||
* Arguments that the command handler should be
|
||||
* invoked with.
|
||||
*
|
||||
* @var mixed[]|null
|
||||
*/
|
||||
public $arguments;
|
||||
}
|
|
@ -1,164 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
use LanguageServer\Definition;
|
||||
|
||||
class CompletionItem
|
||||
{
|
||||
/**
|
||||
* The label of this completion item. By default
|
||||
* also the text that is inserted when selecting
|
||||
* this completion.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $label;
|
||||
|
||||
/**
|
||||
* The kind of this completion item. Based of the kind
|
||||
* an icon is chosen by the editor.
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
public $kind;
|
||||
|
||||
/**
|
||||
* A human-readable string with additional information
|
||||
* about this item, like type or symbol information.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $detail;
|
||||
|
||||
/**
|
||||
* A human-readable string that represents a doc-comment.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $documentation;
|
||||
|
||||
/**
|
||||
* A string that shoud be used when comparing this item
|
||||
* with other items. When `falsy` the label is used.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $sortText;
|
||||
|
||||
/**
|
||||
* A string that should be used when filtering a set of
|
||||
* completion items. When `falsy` the label is used.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $filterText;
|
||||
|
||||
/**
|
||||
* A string that should be inserted a document when selecting
|
||||
* this completion. When `falsy` the label is used.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $insertText;
|
||||
|
||||
/**
|
||||
* An edit which is applied to a document when selecting
|
||||
* this completion. When an edit is provided the value of
|
||||
* insertText is ignored.
|
||||
*
|
||||
* @var TextEdit|null
|
||||
*/
|
||||
public $textEdit;
|
||||
|
||||
/**
|
||||
* An optional array of additional text edits that are applied when
|
||||
* selecting this completion. Edits must not overlap with the main edit
|
||||
* nor with themselves.
|
||||
*
|
||||
* @var TextEdit[]|null
|
||||
*/
|
||||
public $additionalTextEdits;
|
||||
|
||||
/**
|
||||
* An optional command that is executed *after* inserting this completion. *Note* that
|
||||
* additional modifications to the current document should be described with the
|
||||
* additionalTextEdits-property.
|
||||
*
|
||||
* @var Command|null
|
||||
*/
|
||||
public $command;
|
||||
|
||||
/**
|
||||
* An data entry field that is preserved on a completion item between
|
||||
* a completion and a completion resolve request.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
public $data;
|
||||
|
||||
/**
|
||||
* @param string $label
|
||||
* @param int|null $kind
|
||||
* @param string|null $detail
|
||||
* @param string|null $documentation
|
||||
* @param string|null $sortText
|
||||
* @param string|null $filterText
|
||||
* @param string|null $insertText
|
||||
* @param TextEdit|null $textEdit
|
||||
* @param TextEdit[]|null $additionalTextEdits
|
||||
* @param Command|null $command
|
||||
* @param mixed|null $data
|
||||
*/
|
||||
public function __construct(
|
||||
string $label = null,
|
||||
int $kind = null,
|
||||
string $detail = null,
|
||||
string $documentation = null,
|
||||
string $sortText = null,
|
||||
string $filterText = null,
|
||||
string $insertText = null,
|
||||
TextEdit $textEdit = null,
|
||||
array $additionalTextEdits = null,
|
||||
Command $command = null,
|
||||
$data = null
|
||||
) {
|
||||
$this->label = $label;
|
||||
$this->kind = $kind;
|
||||
$this->detail = $detail;
|
||||
$this->documentation = $documentation;
|
||||
$this->sortText = $sortText;
|
||||
$this->filterText = $filterText;
|
||||
$this->insertText = $insertText;
|
||||
$this->textEdit = $textEdit;
|
||||
$this->additionalTextEdits = $additionalTextEdits;
|
||||
$this->command = $command;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a CompletionItem for a Definition
|
||||
*
|
||||
* @param Definition $def
|
||||
* @return self
|
||||
*/
|
||||
public static function fromDefinition(Definition $def): self
|
||||
{
|
||||
$item = new CompletionItem;
|
||||
$item->label = $def->symbolInformation->name;
|
||||
$item->kind = CompletionItemKind::fromSymbolKind($def->symbolInformation->kind);
|
||||
if ($def->type) {
|
||||
$item->detail = (string)$def->type;
|
||||
} else if ($def->symbolInformation->containerName) {
|
||||
$item->detail = $def->symbolInformation->containerName;
|
||||
}
|
||||
if ($def->documentation) {
|
||||
$item->documentation = $def->documentation;
|
||||
}
|
||||
if ($def->isStatic && $def->symbolInformation->kind === SymbolKind::PROPERTY) {
|
||||
$item->insertText = '$' . $def->symbolInformation->name;
|
||||
}
|
||||
return $item;
|
||||
}
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
/**
|
||||
* The kind of a completion entry.
|
||||
*/
|
||||
abstract class CompletionItemKind
|
||||
{
|
||||
const TEXT = 1;
|
||||
const METHOD = 2;
|
||||
const FUNCTION = 3;
|
||||
const CONSTRUCTOR = 4;
|
||||
const FIELD = 5;
|
||||
const VARIABLE = 6;
|
||||
const CLASS_ = 7;
|
||||
const INTERFACE = 8;
|
||||
const MODULE = 9;
|
||||
const PROPERTY = 10;
|
||||
const UNIT = 11;
|
||||
const VALUE = 12;
|
||||
const ENUM = 13;
|
||||
const KEYWORD = 14;
|
||||
const SNIPPET = 15;
|
||||
const COLOR = 16;
|
||||
const FILE = 17;
|
||||
const REFERENCE = 18;
|
||||
|
||||
/**
|
||||
* Returns the CompletionItemKind for a SymbolKind
|
||||
*
|
||||
* @param int $kind A SymbolKind
|
||||
* @return int The CompletionItemKind
|
||||
*/
|
||||
public static function fromSymbolKind(int $kind): int
|
||||
{
|
||||
switch ($kind) {
|
||||
case SymbolKind::PROPERTY:
|
||||
case SymbolKind::FIELD:
|
||||
return self::PROPERTY;
|
||||
case SymbolKind::METHOD:
|
||||
return self::METHOD;
|
||||
case SymbolKind::CLASS_:
|
||||
return self::CLASS_;
|
||||
case SymbolKind::INTERFACE:
|
||||
return self::INTERFACE;
|
||||
case SymbolKind::FUNCTION:
|
||||
return self::FUNCTION;
|
||||
case SymbolKind::NAMESPACE:
|
||||
case SymbolKind::MODULE:
|
||||
case SymbolKind::PACKAGE:
|
||||
return self::MODULE;
|
||||
case SymbolKind::FILE:
|
||||
return self::FILE;
|
||||
case SymbolKind::STRING:
|
||||
return self::TEXT;
|
||||
case SymbolKind::NUMBER:
|
||||
case SymbolKind::BOOLEAN:
|
||||
case SymbolKind::ARRAY:
|
||||
return self::VALUE;
|
||||
case SymbolKind::ENUM:
|
||||
return self::ENUM;
|
||||
case SymbolKind::CONSTRUCTOR:
|
||||
return self::CONSTRUCTOR;
|
||||
case SymbolKind::VARIABLE:
|
||||
case SymbolKind::CONSTANT:
|
||||
return self::VARIABLE;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
/**
|
||||
* Represents a collection of completion items to be presented in
|
||||
* the editor.
|
||||
*/
|
||||
class CompletionList
|
||||
{
|
||||
/**
|
||||
* This list it not complete. Further typing should result in recomputing this
|
||||
* list.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $isIncomplete;
|
||||
|
||||
/**
|
||||
* The completion items.
|
||||
*
|
||||
* @var CompletionItem[]
|
||||
*/
|
||||
public $items;
|
||||
|
||||
/**
|
||||
* @param CompletionItem[] $items The completion items.
|
||||
* @param bool $isIncomplete This list it not complete. Further typing should result in recomputing this list.
|
||||
*/
|
||||
public function __construct(array $items = [], bool $isIncomplete = false)
|
||||
{
|
||||
$this->items = $items;
|
||||
$this->isIncomplete = $isIncomplete;
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
/**
|
||||
* Completion options.
|
||||
*/
|
||||
class CompletionOptions
|
||||
{
|
||||
/*
|
||||
* The server provides support to resolve additional information for a completion
|
||||
* item.
|
||||
*
|
||||
* @var bool|null
|
||||
*/
|
||||
public $resolveProvider;
|
||||
|
||||
/**
|
||||
* The characters that trigger completion automatically.
|
||||
*
|
||||
* @var string[]|null
|
||||
*/
|
||||
public $triggerCharacters;
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
/**
|
||||
* An event describing a change to a text document. If range and rangeLength are
|
||||
* omitted the new text is considered to be the full content of the document.
|
||||
*/
|
||||
class ContentChangeEvent
|
||||
{
|
||||
/**
|
||||
* The range of the document that changed.
|
||||
*
|
||||
* @var Range|null
|
||||
*/
|
||||
public $range;
|
||||
|
||||
/**
|
||||
* The length of the range that got replaced.
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
public $rangeLength;
|
||||
|
||||
/**
|
||||
* The new text of the document.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $text;
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
class DependencyReference
|
||||
{
|
||||
/**
|
||||
* @var mixed
|
||||
*/
|
||||
public $hints;
|
||||
|
||||
/**
|
||||
* @var object
|
||||
*/
|
||||
public $attributes;
|
||||
|
||||
/**
|
||||
* @param object $attributes
|
||||
* @param mixed $hints
|
||||
*/
|
||||
public function __construct($attributes = null, $hints = null)
|
||||
{
|
||||
$this->attributes = $attributes ?? new \stdClass;
|
||||
$this->hints = $hints;
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
/**
|
||||
* Represents a diagnostic, such as a compiler error or warning. Diagnostic objects are only valid in the scope of a
|
||||
* resource.
|
||||
*/
|
||||
class Diagnostic
|
||||
{
|
||||
/**
|
||||
* The range at which the message applies.
|
||||
*
|
||||
* @var Range
|
||||
*/
|
||||
public $range;
|
||||
|
||||
/**
|
||||
* The diagnostic's severity. Can be omitted. If omitted it is up to the
|
||||
* client to interpret diagnostics as error, warning, info or hint.
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
public $severity;
|
||||
|
||||
/**
|
||||
* The diagnostic's code. Can be omitted.
|
||||
*
|
||||
* @var int|string|null
|
||||
*/
|
||||
public $code;
|
||||
|
||||
/**
|
||||
* A human-readable string describing the source of this
|
||||
* diagnostic, e.g. 'typescript' or 'super lint'.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $source;
|
||||
|
||||
/**
|
||||
* The diagnostic's message.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $message;
|
||||
|
||||
/**
|
||||
* @param string $message The diagnostic's message
|
||||
* @param Range $range The range at which the message applies
|
||||
* @param int $code The diagnostic's code
|
||||
* @param int $severity DiagnosticSeverity
|
||||
* @param string $source A human-readable string describing the source of this diagnostic
|
||||
*/
|
||||
public function __construct(string $message = null, Range $range = null, int $code = null, int $severity = null, string $source = null)
|
||||
{
|
||||
$this->message = $message;
|
||||
$this->range = $range;
|
||||
$this->code = $code;
|
||||
$this->severity = $severity;
|
||||
$this->source = $source;
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
abstract class DiagnosticSeverity
|
||||
{
|
||||
/**
|
||||
* Reports an error.
|
||||
*/
|
||||
const ERROR = 1;
|
||||
|
||||
/**
|
||||
* Reports a warning.
|
||||
*/
|
||||
const WARNING = 2;
|
||||
|
||||
/**
|
||||
* Reports an information.
|
||||
*/
|
||||
const INFORMATION = 3;
|
||||
|
||||
/**
|
||||
* Reports a hint.
|
||||
*/
|
||||
const HINT = 4;
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
/**
|
||||
* A document highlight is a range inside a text document which deserves
|
||||
* special attention. Usually a document highlight is visualized by changing
|
||||
* the background color of its range.
|
||||
*/
|
||||
class DocumentHighlightKind
|
||||
{
|
||||
/**
|
||||
* The range this highlight applies to.
|
||||
*
|
||||
* @var Range
|
||||
*/
|
||||
public $range;
|
||||
|
||||
/**
|
||||
* The highlight kind, default is DocumentHighlightKind::TEXT.
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
public $kind;
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
/**
|
||||
* A document highlight kind.
|
||||
*/
|
||||
abstract class DocumentHighlightKind
|
||||
{
|
||||
/**
|
||||
* A textual occurrance.
|
||||
*/
|
||||
const TEXT = 1;
|
||||
|
||||
/**
|
||||
* Read-access of a symbol, like reading a variable.
|
||||
*/
|
||||
const READ = 2;
|
||||
|
||||
/**
|
||||
* Write-access of a symbol, like writing to a variable.
|
||||
*/
|
||||
const WRITE = 3;
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
/**
|
||||
* Format document on type options
|
||||
*/
|
||||
class DocumentOnTypeFormattingOptions
|
||||
{
|
||||
/**
|
||||
* A character on which formatting should be triggered, like `}`.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $firstTriggerCharacter;
|
||||
|
||||
/**
|
||||
* More trigger characters.
|
||||
*
|
||||
* @var string[]|null
|
||||
*/
|
||||
public $moreTriggerCharacter;
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
/**
|
||||
* Enum
|
||||
*/
|
||||
abstract class ErrorCode
|
||||
{
|
||||
const PARSE_ERROR = -32700;
|
||||
const INVALID_REQUEST = -32600;
|
||||
const METHOD_NOT_FOUND = -32601;
|
||||
const INVALID_PARAMS = -32602;
|
||||
const INTERNAL_ERROR = -32603;
|
||||
const SERVER_ERROR_START = -32099;
|
||||
const SERVER_ERROR_END = -32000;
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
/**
|
||||
* The file event type. Enum
|
||||
*/
|
||||
abstract class FileChangeType
|
||||
{
|
||||
/**
|
||||
* The file got created.
|
||||
*/
|
||||
const CREATED = 1;
|
||||
|
||||
/**
|
||||
* The file got changed.
|
||||
*/
|
||||
const CHANGED = 2;
|
||||
|
||||
/**
|
||||
* The file got deleted.
|
||||
*/
|
||||
const DELETED = 3;
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
/**
|
||||
* An event describing a file change.
|
||||
*/
|
||||
class FileEvent
|
||||
{
|
||||
/**
|
||||
* The file's URI.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $uri;
|
||||
|
||||
/**
|
||||
* The change type.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $type;
|
||||
|
||||
/**
|
||||
* @param string $uri
|
||||
* @param int $type
|
||||
*/
|
||||
public function __construct(string $uri, int $type)
|
||||
{
|
||||
$this->uri = $uri;
|
||||
$this->type = $type;
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
/**
|
||||
* Value-object describing what options formatting should use.
|
||||
*/
|
||||
class FormattingOptions
|
||||
{
|
||||
/**
|
||||
* Size of a tab in spaces.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $tabSize;
|
||||
|
||||
/**
|
||||
* Prefer spaces over tabs.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $insertSpaces;
|
||||
|
||||
// Can be extended with further properties.
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
/**
|
||||
* The result of a hover request.
|
||||
*/
|
||||
class Hover
|
||||
{
|
||||
/**
|
||||
* The hover's content
|
||||
*
|
||||
* @var string|MarkedString|string[]|MarkedString[]
|
||||
*/
|
||||
public $contents;
|
||||
|
||||
/**
|
||||
* An optional range
|
||||
*
|
||||
* @var Range|null
|
||||
*/
|
||||
public $range;
|
||||
|
||||
/**
|
||||
* @param string|MarkedString|string[]|MarkedString[] $contents The hover's content
|
||||
* @param Range $range An optional range
|
||||
*/
|
||||
public function __construct($contents = null, $range = null)
|
||||
{
|
||||
$this->contents = $contents;
|
||||
$this->range = $range;
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
class InitializeResult
|
||||
{
|
||||
/**
|
||||
* The capabilities the language server provides.
|
||||
*
|
||||
* @var LanguageServer\Protocol\ServerCapabilities
|
||||
*/
|
||||
public $capabilities;
|
||||
|
||||
/**
|
||||
* @param LanguageServer\Protocol\ServerCapabilities $capabilities
|
||||
*/
|
||||
public function __construct(ServerCapabilities $capabilities = null)
|
||||
{
|
||||
$this->capabilities = $capabilities ?? new ServerCapabilities();
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
use Microsoft\PhpParser;
|
||||
use Microsoft\PhpParser\Node;
|
||||
|
||||
/**
|
||||
* Represents a location inside a resource, such as a line inside a text file.
|
||||
*/
|
||||
class Location
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $uri;
|
||||
|
||||
/**
|
||||
* @var Range
|
||||
*/
|
||||
public $range;
|
||||
|
||||
/**
|
||||
* Returns the location of the node
|
||||
*
|
||||
* @param Node $node
|
||||
* @return self
|
||||
*/
|
||||
public static function 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)
|
||||
{
|
||||
$this->uri = $uri;
|
||||
$this->range = $range;
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
class MarkedString
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $language;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $value;
|
||||
|
||||
public function __construct(string $language = null, string $value = null)
|
||||
{
|
||||
$this->language = $language;
|
||||
$this->value = $value;
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
class MessageActionItem
|
||||
{
|
||||
/**
|
||||
* A short title like 'Retry', 'Open Log' etc.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $title;
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
/**
|
||||
* Enum
|
||||
*/
|
||||
abstract class MessageType
|
||||
{
|
||||
/**
|
||||
* An error message.
|
||||
*/
|
||||
const ERROR = 1;
|
||||
|
||||
/**
|
||||
* A warning message.
|
||||
*/
|
||||
const WARNING = 2;
|
||||
|
||||
/**
|
||||
* An information message.
|
||||
*/
|
||||
const INFO = 3;
|
||||
|
||||
/**
|
||||
* A log message.
|
||||
*/
|
||||
const LOG = 4;
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
/**
|
||||
* Represents a parameter of a callable-signature. A parameter can
|
||||
* have a label and a doc-comment.
|
||||
*/
|
||||
class ParameterInformation
|
||||
{
|
||||
/**
|
||||
* The label of this signature. Will be shown in
|
||||
* the UI.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $label;
|
||||
|
||||
/**
|
||||
* The human-readable doc-comment of this signature. Will be shown
|
||||
* in the UI but can be omitted.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $documentation;
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
/**
|
||||
* Position in a text document expressed as zero-based line and character offset.
|
||||
*/
|
||||
class Position
|
||||
{
|
||||
/**
|
||||
* Line position in a document (zero-based).
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $line;
|
||||
|
||||
/**
|
||||
* Character offset on a line in a document (zero-based).
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $character;
|
||||
|
||||
public function __construct(int $line = null, int $character = null)
|
||||
{
|
||||
$this->line = $line;
|
||||
$this->character = $character;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares this position to another position
|
||||
* Returns
|
||||
* - 0 if the positions match
|
||||
* - a negative number if $this is before $position
|
||||
* - a positive number otherwise
|
||||
*
|
||||
* @param Position $position
|
||||
* @return int
|
||||
*/
|
||||
public function compare(Position $position): int
|
||||
{
|
||||
if ($this->line === $position->line && $this->character === $position->character) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($this->line !== $position->line) {
|
||||
return $this->line - $position->line;
|
||||
}
|
||||
|
||||
return $this->character - $position->character;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the offset of the position in a string
|
||||
*
|
||||
* @param string $content
|
||||
* @return int
|
||||
*/
|
||||
public function toOffset(string $content): int
|
||||
{
|
||||
$lines = explode("\n", $content);
|
||||
$slice = array_slice($lines, 0, $this->line);
|
||||
return array_sum(array_map('strlen', $slice)) + count($slice) + $this->character;
|
||||
}
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
use Microsoft\PhpParser;
|
||||
use Microsoft\PhpParser\Node;
|
||||
|
||||
/**
|
||||
* A range in a text document expressed as (zero-based) start and end positions.
|
||||
*/
|
||||
class Range
|
||||
{
|
||||
/**
|
||||
* The range's start position.
|
||||
*
|
||||
* @var Position
|
||||
*/
|
||||
public $start;
|
||||
|
||||
/**
|
||||
* The range's end position.
|
||||
*
|
||||
* @var Position
|
||||
*/
|
||||
public $end;
|
||||
|
||||
/**
|
||||
* Returns the range the node spans
|
||||
*
|
||||
* @param Node $node
|
||||
* @return self
|
||||
*/
|
||||
public static function fromNode(Node $node)
|
||||
{
|
||||
$range = PhpParser\PositionUtilities::getRangeFromPosition($node->getStart(), $node->getWidth(), $node->getFileContents());
|
||||
|
||||
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)
|
||||
{
|
||||
$this->start = $start;
|
||||
$this->end = $end;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a position is within the range
|
||||
*
|
||||
* @param Position $position
|
||||
* @return bool
|
||||
*/
|
||||
public function includes(Position $position): bool
|
||||
{
|
||||
return $this->start->compare($position) <= 0 && $this->end->compare($position) >= 0;
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
class ReferenceContext
|
||||
{
|
||||
/**
|
||||
* Include the declaration of the current symbol.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $includeDeclaration;
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
/**
|
||||
* Metadata about the symbol that can be used to identify or locate its
|
||||
* definition.
|
||||
*/
|
||||
class ReferenceInformation
|
||||
{
|
||||
/**
|
||||
* The location in the workspace where the `symbol` is referenced.
|
||||
*
|
||||
* @var Location
|
||||
*/
|
||||
public $reference;
|
||||
|
||||
/**
|
||||
* Metadata about the symbol that can be used to identify or locate its
|
||||
* definition.
|
||||
*
|
||||
* @var SymbolDescriptor
|
||||
*/
|
||||
public $symbol;
|
||||
|
||||
/**
|
||||
* @param Location $reference The location in the workspace where the `symbol` is referenced.
|
||||
* @param SymbolDescriptor $symbol Metadata about the symbol that can be used to identify or locate its definition.
|
||||
*/
|
||||
public function __construct(Location $reference = null, SymbolDescriptor $symbol = null)
|
||||
{
|
||||
$this->reference = $reference;
|
||||
$this->symbol = $symbol;
|
||||
}
|
||||
}
|
|
@ -1,132 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
class ServerCapabilities
|
||||
{
|
||||
/**
|
||||
* Defines how text documents are synced.
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
public $textDocumentSync;
|
||||
|
||||
/**
|
||||
* The server provides hover support.
|
||||
*
|
||||
* @var bool|null
|
||||
*/
|
||||
public $hoverProvider;
|
||||
|
||||
/**
|
||||
* The server provides completion support.
|
||||
*
|
||||
* @var CompletionOptions|null
|
||||
*/
|
||||
public $completionProvider;
|
||||
|
||||
/**
|
||||
* The server provides signature help support.
|
||||
*
|
||||
* @var SignatureHelpOptions|null
|
||||
*/
|
||||
public $signatureHelpProvider;
|
||||
|
||||
/**
|
||||
* The server provides goto definition support.
|
||||
*
|
||||
* @var bool|null
|
||||
*/
|
||||
public $definitionProvider;
|
||||
|
||||
/**
|
||||
* The server provides find references support.
|
||||
*
|
||||
* @var bool|null
|
||||
*/
|
||||
public $referencesProvider;
|
||||
|
||||
/**
|
||||
* The server provides document highlight support.
|
||||
*
|
||||
* @var bool|null
|
||||
*/
|
||||
public $documentHighlightProvider;
|
||||
|
||||
/**
|
||||
* The server provides document symbol support.
|
||||
*
|
||||
* @var bool|null
|
||||
*/
|
||||
public $documentSymbolProvider;
|
||||
|
||||
/**
|
||||
* The server provides workspace symbol support.
|
||||
*
|
||||
* @var bool|null
|
||||
*/
|
||||
public $workspaceSymbolProvider;
|
||||
|
||||
/**
|
||||
* The server provides code actions.
|
||||
*
|
||||
* @var bool|null
|
||||
*/
|
||||
public $codeActionProvider;
|
||||
|
||||
/**
|
||||
* The server provides code lens.
|
||||
*
|
||||
* @var CodeLensOptions|null
|
||||
*/
|
||||
public $codeLensProvider;
|
||||
|
||||
/**
|
||||
* The server provides document formatting.
|
||||
*
|
||||
* @var bool|null
|
||||
*/
|
||||
public $documentFormattingProvider;
|
||||
|
||||
/**
|
||||
* The server provides document range formatting.
|
||||
*
|
||||
* @var bool|null
|
||||
*/
|
||||
public $documentRangeFormattingProvider;
|
||||
|
||||
/**
|
||||
* The server provides document formatting on typing.
|
||||
*
|
||||
* @var DocumentOnTypeFormattingOptions|null
|
||||
*/
|
||||
public $documentOnTypeFormattingProvider;
|
||||
|
||||
/**
|
||||
* The server provides rename support.
|
||||
*
|
||||
* @var bool|null
|
||||
*/
|
||||
public $renameProvider;
|
||||
|
||||
/**
|
||||
* The server provides workspace references exporting support.
|
||||
*
|
||||
* @var bool|null
|
||||
*/
|
||||
public $xworkspaceReferencesProvider;
|
||||
|
||||
/**
|
||||
* The server provides extended text document definition support.
|
||||
*
|
||||
* @var bool|null
|
||||
*/
|
||||
public $xdefinitionProvider;
|
||||
|
||||
/**
|
||||
* The server provides workspace dependencies support.
|
||||
*
|
||||
* @var bool|null
|
||||
*/
|
||||
public $dependenciesProvider;
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
/**
|
||||
* Signature help represents the signature of something
|
||||
* callable. There can be multiple signature but only one
|
||||
* active and only one active parameter.
|
||||
*/
|
||||
class SignatureHelp
|
||||
{
|
||||
/**
|
||||
* One or more signatures.
|
||||
*
|
||||
* @var SignatureInformation[]
|
||||
*/
|
||||
public $signatures;
|
||||
|
||||
/**
|
||||
* The active signature.
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
public $activeSignature;
|
||||
|
||||
/**
|
||||
* The active parameter of the active signature.
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
public $activeParameter;
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
/**
|
||||
* Signature help options.
|
||||
*/
|
||||
class SignatureHelpOptions
|
||||
{
|
||||
/**
|
||||
* The characters that trigger signature help automatically.
|
||||
*
|
||||
* @var string[]|null
|
||||
*/
|
||||
public $triggerCharacters;
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
/**
|
||||
* Represents the signature of something callable. A signature
|
||||
* can have a label, like a function-name, a doc-comment, and
|
||||
* a set of parameters.
|
||||
*/
|
||||
class SignatureInformation
|
||||
{
|
||||
/**
|
||||
* The label of this signature. Will be shown in
|
||||
* the UI.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $label;
|
||||
|
||||
/**
|
||||
* The human-readable doc-comment of this signature. Will be shown
|
||||
* in the UI but can be omitted.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $documentation;
|
||||
|
||||
/**
|
||||
* The parameters of this signature.
|
||||
*
|
||||
* @var ParameterInformation[]|null
|
||||
*/
|
||||
public $parameters;
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
class SymbolDescriptor extends SymbolInformation
|
||||
{
|
||||
/**
|
||||
* The fully qualified structural element name, a globally unique identifier for the symbol.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @var object|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
|
||||
*/
|
||||
public function __construct(string $fqsen = null, $package = null)
|
||||
{
|
||||
$this->fqsen = $fqsen;
|
||||
$this->package = $package;
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Protocol;
|
||||
|
||||
/**
|
||||
* A symbol kind.
|
||||
*/
|
||||
abstract class SymbolKind
|
||||
{
|
||||
const FILE = 1;
|
||||
const MODULE = 2;
|
||||
const NAMESPACE = 3;
|
||||
const PACKAGE = 4;
|
||||
const CLASS_ = 5;
|
||||
const METHOD = 6;
|
||||
const PROPERTY = 7;
|
||||
const FIELD = 8;
|
||||
const CONSTRUCTOR = 9;
|
||||
const ENUM = 10;
|
||||
const INTERFACE = 11;
|
||||
const FUNCTION = 12;
|
||||
const VARIABLE = 13;
|
||||
const CONSTANT = 14;
|
||||
const STRING = 15;
|
||||
const NUMBER = 16;
|
||||
const BOOLEAN = 17;
|
||||
const ARRAY = 18;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue