Compare commits
298 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 | |
|
a772d9a2d7 | |
|
0e3727a8d6 | |
|
663ccd5f23 | |
|
4a98afe540 | |
|
3b633369a7 | |
|
8d1732ed02 | |
|
fe7e9d5800 | |
|
4c1d7bd1bc | |
|
cc3f0da21a | |
|
f10680e441 | |
|
7b72b38fd9 | |
|
42d0c7b714 | |
|
7f427a1215 | |
|
08cf1a3fd7 | |
|
b1cc7bf6b0 | |
|
de6aed608c | |
|
97d1579f37 | |
|
f50df5cdaf | |
|
14a6d65832 | |
|
4d0a0a2a10 | |
|
546660f957 | |
|
7f8eccb5ae | |
|
0de7ba8335 | |
|
56bd465bf8 | |
|
cbfd70d398 | |
|
5d2ab8f369 | |
|
3856f4f46a | |
|
d5c54ac30f | |
|
571b26a0c3 | |
|
3c11cde9fb | |
|
5100d89617 | |
|
b90ede7fb3 | |
|
bedd157636 | |
|
34d3d2030d | |
|
7fbd68a61a | |
|
d8823bc7dc | |
|
47b5b6709c | |
|
96aa998486 | |
|
43a91b0d09 | |
|
d080c161a9 | |
|
106aa24b5d | |
|
49245fd4d3 | |
|
662143abad | |
|
de0dd32a67 | |
|
b93d4f33cb | |
|
710d2a7ff7 | |
|
2005518dfe | |
|
83618fee2e | |
|
2242a35678 | |
|
d03db024c1 | |
|
a4a13e6528 | |
|
cc9d5e987b | |
|
cd116a252b | |
|
0b61951a9c | |
|
96ea8608d7 | |
|
b8a113ddd0 | |
|
d90a88e625 | |
|
d7fc9e0425 | |
|
a7d77d844e | |
|
b9f9871156 | |
|
ebd1cc6133 | |
|
db6f4f7e5d | |
|
10fb3c92e0 | |
|
f56b14438b | |
|
5077b1a87a | |
|
48e0167060 | |
|
ea92b224cd | |
|
e8ab8aa2b8 | |
|
429114ff97 | |
|
5213940064 | |
|
8f6ee8dd02 | |
|
00bc8537a6 | |
|
fb84741d55 | |
|
c2ae7cd022 | |
|
33211c68ca | |
|
c19aedcef2 | |
|
e254e66878 | |
|
5a8d64c18c | |
|
4b014154ac | |
|
50490d51ea | |
|
500ae5dc92 | |
|
12df6a7dd6 | |
|
32b01afa90 | |
|
6056f39d01 | |
|
601c9ad997 | |
|
642425dede | |
|
03bbf5f4ba | |
|
25f300c157 | |
|
47472252a7 | |
|
9e65cd4cf0 | |
|
04ef6c8adf | |
|
ff0a35d833 | |
|
bec24383d4 | |
|
28dc42b5c0 | |
|
92145c526e | |
|
99224b73e4 | |
|
18e58b4ce8 | |
|
ed41df0062 | |
|
867196babf | |
|
8a354ba1af | |
|
6806ba94e0 | |
|
cd3bf18fe2 | |
|
5ecab683eb | |
|
83afa0c1b8 | |
|
a19d225a7a | |
|
b16674d394 | |
|
b9222b0fd1 | |
|
1e7260a2ea | |
|
8e36e59e9a | |
|
953a8023b7 | |
|
f8733c741c | |
|
1e00275e02 | |
|
9cbca1cd7f | |
|
96694996f7 | |
|
e993b9994a | |
|
e19670c141 | |
|
6bd1b10e4d | |
|
5f984e2826 | |
|
44445e3af4 | |
|
7668a0c695 | |
|
4db7ffd88c | |
|
2e03aa32f3 | |
|
6b6ec8c105 | |
|
cdcfaf7849 | |
|
691a0bddfe | |
|
cba4357856 | |
|
3290ec31b2 | |
|
1e6917ef55 | |
|
2980941fd1 | |
|
c479969758 | |
|
6fe01183b0 | |
|
66b5176a43 | |
|
f81d03948b | |
|
8f7f975408 | |
|
c667f83371 | |
|
23b127a986 | |
|
15e004fb9b | |
|
d41cde2039 | |
|
1f808c59e1 | |
|
e75c1592fc | |
|
18ac760bc6 | |
|
03e4e34a4e | |
|
aff9edb630 | |
|
1689e4d0dc | |
|
a74bf90d77 | |
|
7032f806d4 | |
|
0387f28759 | |
|
7f95b76cf8 | |
|
7322a6c658 | |
|
3a880934e5 | |
|
6be53ad658 | |
|
d4757e0a24 | |
|
827ab4c842 | |
|
fbdf1aa414 | |
|
987308fc0a | |
|
48c71e5bc1 | |
|
4786fe173c | |
|
658a27f5a5 | |
|
063c7f9ad2 | |
|
6cb916e28d | |
|
c962f81924 | |
|
0c758ec815 | |
|
dfc80a5c66 | |
|
5bc228a8e0 | |
|
6917f1c789 | |
|
9fd9a02e19 | |
|
501d26e1d4 | |
|
bc2d6b9b59 | |
|
41e9fb7e8a | |
|
817056270e | |
|
4d5052bebd | |
|
2d4ca8f99a | |
|
a20a86c9b9 | |
|
72d776d638 | |
|
7138088b4f | |
|
aa6b729336 | |
|
4fc2a6c2e4 | |
|
6169998b92 | |
|
d1b9b33741 | |
|
4e88a17de3 | |
|
23f641f78b | |
|
db28e22378 | |
|
57604e61f1 | |
|
b8b038d0b0 | |
|
6183243b18 | |
|
284bde2e36 | |
|
21034df05b |
|
@ -0,0 +1,10 @@
|
|||
.DS_Store
|
||||
.vscode/
|
||||
.idea/
|
||||
.git/
|
||||
tests/
|
||||
fixtures/
|
||||
coverage/
|
||||
coverage.xml
|
||||
images/
|
||||
node_modules/
|
|
@ -7,11 +7,11 @@ trim_trailing_whitespace = true
|
|||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.json,*.yml]
|
||||
[*.{json,yml}]
|
||||
indent_size = 2
|
||||
|
||||
[composer.json]
|
||||
indent_size = 4
|
||||
|
||||
[*.md]
|
||||
[{*.md,fixtures/**}]
|
||||
trim_trailing_whitespace = false
|
||||
|
|
|
@ -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,4 +1,8 @@
|
|||
.DS_Store
|
||||
.vscode
|
||||
.idea
|
||||
vendor/
|
||||
.phpls/
|
||||
composer.lock
|
||||
stubs
|
||||
*.ast
|
||||
node_modules/
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
[submodule "validation/frameworks/php-language-server"]
|
||||
path = validation/frameworks/php-language-server
|
||||
url = https://github.com/felixfbecker/php-language-server
|
||||
[submodule "validation/frameworks/wordpress"]
|
||||
path = validation/frameworks/wordpress
|
||||
url = https://github.com/wordpress/wordpress
|
||||
[submodule "validation/frameworks/drupal"]
|
||||
path = validation/frameworks/drupal
|
||||
url = https://github.com/drupal/drupal
|
||||
[submodule "validation/frameworks/tolerant-php-parser"]
|
||||
path = validation/frameworks/tolerant-php-parser
|
||||
url = https://github.com/microsoft/tolerant-php-parser
|
||||
[submodule "validation/frameworks/symfony"]
|
||||
path = validation/frameworks/symfony
|
||||
url = https://github.com/symfony/symfony
|
||||
[submodule "validation/frameworks/math-php"]
|
||||
path = validation/frameworks/math-php
|
||||
url = https://github.com/markrogoyski/math-php
|
||||
[submodule "validation/frameworks/codeigniter"]
|
||||
path = validation/frameworks/codeigniter
|
||||
url = https://github.com/bcit-ci/codeigniter
|
||||
[submodule "validation/frameworks/cakephp"]
|
||||
path = validation/frameworks/cakephp
|
||||
url = https://github.com/cakephp/cakephp
|
||||
[submodule "validation/frameworks/phpunit"]
|
||||
path = validation/frameworks/phpunit
|
||||
url = https://github.com/sebastianbergmann/phpunit
|
|
@ -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',
|
||||
],
|
||||
];
|
56
.travis.yml
|
@ -1,10 +1,60 @@
|
|||
language: php
|
||||
|
||||
php:
|
||||
- '7.0'
|
||||
- '7.0'
|
||||
- '7.2'
|
||||
|
||||
git:
|
||||
depth: 10
|
||||
submodules: false
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/Library/Caches/Homebrew
|
||||
- $HOME/.composer/cache
|
||||
- $HOME/.npm
|
||||
|
||||
install:
|
||||
- composer install
|
||||
- composer install --prefer-dist --no-interaction
|
||||
- pecl install ast-1.0.0
|
||||
|
||||
script:
|
||||
- vendor/bin/phpunit --bootstrap vendor/autoload.php tests
|
||||
- vendor/bin/phpcs -n
|
||||
- vendor/bin/phan
|
||||
- vendor/bin/phpunit --coverage-clover=coverage.xml --colors=always
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
# 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
|
||||
|
||||
FROM composer AS builder
|
||||
|
||||
COPY ./ /app
|
||||
RUN composer install
|
||||
|
||||
FROM php:7-cli
|
||||
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 --from=builder /app /srv/phpls
|
||||
|
||||
WORKDIR /srv/phpls
|
||||
|
||||
EXPOSE 2088
|
||||
|
||||
CMD ["--tcp-server=0:2088"]
|
||||
|
||||
ENTRYPOINT ["php", "bin/php-language-server.php"]
|
|
@ -0,0 +1,15 @@
|
|||
ISC License
|
||||
|
||||
Copyright (c) 2016, Felix Frederick Becker
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
224
README.md
|
@ -1,7 +1,225 @@
|
|||
# PHP Language Server
|
||||
|
||||
[](https://packagist.org/packages/felixfbecker/language-server)
|
||||
[](https://travis-ci.org/felixfbecker/php-language-server)
|
||||
[](https://packagist.org/packages/felixfbecker/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://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)
|
||||
|
||||
A pure PHP implementation of the [Language Server Protocol](https://github.com/Microsoft/language-server-protocol).
|
||||
A pure PHP implementation of the open [Language Server Protocol](https://github.com/Microsoft/language-server-protocol).
|
||||
Provides static code analysis for PHP for any IDE.
|
||||
|
||||
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)
|
||||

|
||||
|
||||
### [Find References](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#find-references-request)
|
||||

|
||||
|
||||
### [Hover](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#hover-request)
|
||||

|
||||
|
||||

|
||||
|
||||
A hover request returns a declaration line (marked with language `php`) and the summary of the docblock.
|
||||
For Parameters, it will return the `@param` tag.
|
||||
|
||||
### [Document Symbols](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#document-symbols-request)
|
||||

|
||||
|
||||
### [Workspace Symbols](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#workspace-symbols-request)
|
||||

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

|
||||
|
||||
PHP parse errors are reported as errors, parse errors of docblocks are reported as warnings.
|
||||
Errors/Warnings from the `vendor` directory are ignored.
|
||||
|
||||
### Stubs for PHP built-ins
|
||||
|
||||
Completion, type resolval etc. will use the standard PHP library and common extensions.
|
||||
|
||||
### What is considered a definition?
|
||||
|
||||
Globally searchable definitions are:
|
||||
- classes
|
||||
- interfaces
|
||||
- traits
|
||||
- properties
|
||||
- methods
|
||||
- class constants
|
||||
- constants with `const` keyword
|
||||
|
||||
Definitions resolved just-in-time when needed:
|
||||
- variable assignments
|
||||
- parameters
|
||||
- closure `use` statements
|
||||
|
||||
Not supported yet:
|
||||
- constants with `define()`
|
||||
|
||||
Namespaces are not considerd a declaration by design because they only make up a part of the fully qualified name
|
||||
and don't map to one unique declaration.
|
||||
|
||||
### What is considered a reference?
|
||||
|
||||
Definitions/references/hover currently work for
|
||||
- class instantiations
|
||||
- static method calls
|
||||
- class constant access
|
||||
- static property access
|
||||
- parameter type hints
|
||||
- return type hints
|
||||
- method calls, if the variable was assigned to a new object in the same scope
|
||||
- property access, if the variable was assigned to a new object in the same scope
|
||||
- variables
|
||||
- parameters
|
||||
- imported closure variables (`use`)
|
||||
- `use` statements for classes, constants and functions
|
||||
- class-like after `implements`/`extends`
|
||||
- function calls
|
||||
- constant access
|
||||
- `instanceof` checks
|
||||
- Reassigned variables
|
||||
- Nested access/calls on return values, properties, array access
|
||||
|
||||
### Protocol Extensions
|
||||
|
||||
This language server implements the [files protocol extension](https://github.com/sourcegraph/language-server-protocol/blob/master/extension-files.md).
|
||||
If the client expresses support through `ClientCapabilities.xfilesProvider` and `ClientCapabilities.xcontentProvider`,
|
||||
the server will request files in the workspace and file contents through requests from the client and never access
|
||||
the file system directly. This allows the server to operate in an isolated environment like a container,
|
||||
on a remote workspace or any a different protocol than `file://`.
|
||||
|
||||
## Performance
|
||||
|
||||
Upon initialization, the server will recursively scan the project directory for PHP files, parse them and add all definitions
|
||||
and references to an in-memory index.
|
||||
The time this takes depends on the project size.
|
||||
At the time of writing, this project contains 78 files + 1560 files in dependencies which take 97s to parse
|
||||
and consume 76 MB on a Surface Pro 3.
|
||||
The language server is fully operational while indexing and can respond to requests with the definitions already indexed.
|
||||
Follow-up requests will be almost instant because the index is kept in memory.
|
||||
|
||||
Having XDebug enabled heavily impacts performance and can even crash the server if the `max_nesting_level` setting is too low.
|
||||
|
||||
## Versioning
|
||||
|
||||
This project follows [semver](http://semver.org/) for the protocol communication and command line parameters,
|
||||
e.g. a major version increase of the LSP will result in a major version increase of the PHP LS.
|
||||
New features like request implementations will result in a new minor version.
|
||||
Everything else will be a patch release.
|
||||
All classes are considered internal and are not subject to semver.
|
||||
|
||||
## Installation
|
||||
|
||||
The recommended installation method is through [Composer](https://getcomposer.org/).
|
||||
Simply run
|
||||
|
||||
composer require felixfbecker/language-server
|
||||
|
||||
and you will get the latest stable release and all dependencies.
|
||||
Running `composer update` will update the server to the latest non-breaking version.
|
||||
|
||||
After installing the language server and its dependencies,
|
||||
you must parse the stubs for standard PHP symbols and save the index for fast initialization.
|
||||
|
||||
composer run-script --working-dir=vendor/felixfbecker/language-server parse-stubs
|
||||
|
||||
## Running
|
||||
|
||||
Start the language server with
|
||||
|
||||
php vendor/felixfbecker/language-server/bin/php-language-server.php
|
||||
|
||||
### Command line arguments
|
||||
|
||||
#### `--tcp=host:port` (optional)
|
||||
Causes the server to use a tcp connection for communicating with the language client instead of using STDIN/STDOUT.
|
||||
The server will try to connect to the specified address.
|
||||
Strongly recommended on Windows because of blocking STDIO.
|
||||
|
||||
Example:
|
||||
|
||||
php bin/php-language-server.php --tcp=127.0.0.1:12345
|
||||
|
||||
#### `--tcp-server=host:port` (optional)
|
||||
Causes the server to use a tcp connection for communicating with the language client instead of using STDIN/STDOUT.
|
||||
The server will listen on the given address for a connection.
|
||||
If PCNTL is available, will fork a child process for every connection.
|
||||
If not, will only accept one connection and the connection cannot be reestablished once closed, spawn a new process instead.
|
||||
|
||||
Example:
|
||||
|
||||
php bin/php-language-server.php --tcp-server=127.0.0.1:12345
|
||||
|
||||
#### `--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.
|
||||
The default is 4GB (which is way more than needed).
|
||||
|
||||
Example:
|
||||
|
||||
php bin/php-language-server.php --memory-limit=256M
|
||||
|
||||
## Used by
|
||||
- [VS Code PHP IntelliSense](https://github.com/felixfbecker/vscode-php-intellisense)
|
||||
- [Eclipse Che](https://eclipse.org/che/)
|
||||
- [Eclipse IDE (LSP4E-PHP)](https://github.com/eclipselabs/lsp4e-php)
|
||||
- NeoVim: [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
|
||||
|
||||
You need at least PHP 7.0 and Composer installed.
|
||||
Clone the repository and run
|
||||
|
||||
composer install
|
||||
|
||||
to install dependencies.
|
||||
|
||||
Run the tests with
|
||||
|
||||
composer test
|
||||
|
||||
Lint with
|
||||
|
||||
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;
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Tests;
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use Composer\XdebugHandler\XdebugHandler;
|
||||
use Exception;
|
||||
use LanguageServer\DefinitionResolver;
|
||||
use LanguageServer\Index\Index;
|
||||
use LanguageServer\PhpDocument;
|
||||
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"];
|
||||
|
||||
foreach($frameworks as $framework) {
|
||||
$iterator = new RecursiveDirectoryIterator(__DIR__ . "/../validation/frameworks/$framework");
|
||||
$testProviderArray = array();
|
||||
|
||||
foreach (new RecursiveIteratorIterator($iterator) as $file) {
|
||||
if (strpos((string)$file, ".php") !== false) {
|
||||
$totalSize += $file->getSize();
|
||||
$testProviderArray[] = $file->getPathname();
|
||||
}
|
||||
}
|
||||
|
||||
if (count($testProviderArray) === 0) {
|
||||
throw new Exception("ERROR: Validation testsuite frameworks not found - run `git submodule update --init --recursive` to download.");
|
||||
}
|
||||
|
||||
$start = microtime(true);
|
||||
|
||||
foreach ($testProviderArray as $idx => $testCaseFile) {
|
||||
if (filesize($testCaseFile) > 10000) {
|
||||
continue;
|
||||
}
|
||||
if ($idx % 500 === 0) {
|
||||
echo $idx . '/' . count($testProviderArray) . PHP_EOL;
|
||||
}
|
||||
|
||||
$fileContents = file_get_contents($testCaseFile);
|
||||
|
||||
$docBlockFactory = DocBlockFactory::createInstance();
|
||||
$index = new Index;
|
||||
$maxRecursion = [];
|
||||
$definitions = [];
|
||||
|
||||
$definitionResolver = new DefinitionResolver($index);
|
||||
$parser = new PhpParser\Parser();
|
||||
|
||||
$document = new PhpDocument($testCaseFile, $fileContents, $index, $parser, $docBlockFactory, $definitionResolver);
|
||||
}
|
||||
|
||||
echo "------------------------------\n";
|
||||
|
||||
echo "Time [$framework]: " . (microtime(true) - $start) . PHP_EOL;
|
||||
}
|
||||
|
|
@ -1,7 +1,12 @@
|
|||
<?php
|
||||
|
||||
use LanguageServer\{LanguageServer, ProtocolStreamReader, ProtocolStreamWriter};
|
||||
use LanguageServer\{LanguageServer, ProtocolStreamReader, ProtocolStreamWriter, StderrLogger};
|
||||
use Sabre\Event\Loop;
|
||||
use Composer\XdebugHandler\XdebugHandler;
|
||||
|
||||
$options = getopt('', ['tcp::', 'tcp-server::', 'memory-limit::']);
|
||||
|
||||
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)) {
|
||||
|
@ -10,6 +15,96 @@ foreach ([__DIR__ . '/../../../autoload.php', __DIR__ . '/../autoload.php', __DI
|
|||
}
|
||||
}
|
||||
|
||||
$server = new LanguageServer(new ProtocolStreamReader(STDIN), new ProtocolStreamWriter(STDOUT));
|
||||
// Convert all errors to ErrorExceptions
|
||||
set_error_handler(function (int $severity, string $message, string $file, int $line) {
|
||||
if (!(error_reporting() & $severity)) {
|
||||
// This error code is not included in error_reporting (can also be caused by the @ operator)
|
||||
return;
|
||||
}
|
||||
throw new \ErrorException($message, 0, $severity, $file, $line);
|
||||
});
|
||||
|
||||
Loop\run();
|
||||
$logger = new StderrLogger();
|
||||
|
||||
// Only write uncaught exceptions to STDERR, not STDOUT
|
||||
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
|
||||
$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) {
|
||||
$logger->critical("Could not connect to language client. Error $errno\n$errstr");
|
||||
exit(1);
|
||||
}
|
||||
stream_set_blocking($socket, false);
|
||||
$ls = new LanguageServer(
|
||||
new ProtocolStreamReader($socket),
|
||||
new ProtocolStreamWriter($socket)
|
||||
);
|
||||
Loop\run();
|
||||
} else if (!empty($options['tcp-server'])) {
|
||||
// Run a TCP Server
|
||||
$address = $options['tcp-server'];
|
||||
$tcpServer = stream_socket_server('tcp://' . $address, $errno, $errstr);
|
||||
if ($tcpServer === false) {
|
||||
$logger->critical("Could not listen on $address. Error $errno\n$errstr");
|
||||
exit(1);
|
||||
}
|
||||
$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)) {
|
||||
$logger->debug('Connection accepted');
|
||||
stream_set_blocking($socket, false);
|
||||
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) {
|
||||
$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 () use ($logger) {
|
||||
$logger->debug('Connection closed');
|
||||
});
|
||||
$ls = new LanguageServer($reader, $writer);
|
||||
Loop\run();
|
||||
// Just for safety
|
||||
exit(0);
|
||||
}
|
||||
} else {
|
||||
// If PCNTL is not available, we only accept one connection.
|
||||
// An exit notification will terminate the server
|
||||
$ls = new LanguageServer(
|
||||
new ProtocolStreamReader($socket),
|
||||
new ProtocolStreamWriter($socket)
|
||||
);
|
||||
Loop\run();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Use STDIO
|
||||
$logger->debug('Listening on STDIN');
|
||||
stream_set_blocking(STDIN, false);
|
||||
$ls = new LanguageServer(
|
||||
new ProtocolStreamReader(STDIN),
|
||||
new ProtocolStreamWriter(STDOUT)
|
||||
);
|
||||
Loop\run();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: auto
|
||||
threshold: null
|
||||
base: auto
|
||||
|
||||
comment:
|
||||
layout: "header, diff, tree, changes"
|
||||
behavior: default
|
||||
require_changes: false # if true: only post the comment if coverage changes
|
||||
branches: null
|
||||
flags: null
|
||||
paths: null
|
|
@ -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,63 @@
|
|||
"autocompletion",
|
||||
"refactor"
|
||||
],
|
||||
"bin": ["bin/php-language-server.php"],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Felix Becker",
|
||||
"email": "felix.b@outlook.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.0",
|
||||
"nikic/php-parser": "3.0.0alpha1",
|
||||
"phpdocumentor/reflection-docblock": "^3.0",
|
||||
"sabre/event": "^3.0",
|
||||
"felixfbecker/advanced-json-rpc": "^1.2"
|
||||
"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",
|
||||
"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/"
|
||||
}
|
||||
},
|
||||
"files" : [
|
||||
"src/utils.php",
|
||||
"src/FqnUtilities.php",
|
||||
"src/ParserHelpers.php"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"LanguageServer\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^5.5"
|
||||
}
|
||||
"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,5 @@
|
|||
<?php
|
||||
|
||||
$abc = 1;
|
||||
$abc2 = 2;
|
||||
echo $ab
|
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
|
||||
TestClass::TE
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace HELLO {
|
||||
|
||||
/**
|
||||
* Does something really cool!
|
||||
*/
|
||||
function world() {
|
||||
|
||||
}
|
||||
|
||||
\HE
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* Lorem ipsum dolor sit amet.
|
||||
*/
|
||||
define('HELLO', true);
|
||||
|
||||
HELLO\world();
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
namespace Foo;
|
||||
|
||||
class Bar {
|
||||
public $foo;
|
||||
|
||||
/** @return Bar[] */
|
||||
public function test() { }
|
||||
}
|
||||
|
||||
$bar = new Bar();
|
||||
$bars = $bar->test();
|
||||
$array1 = [new Bar(), new \stdClass()];
|
||||
$array2 = ['foo' => $bar, $bar];
|
||||
$array3 = ['foo' => $bar, 'baz' => $bar];
|
||||
|
||||
foreach ($bars as $value) {
|
||||
$v
|
||||
$value->
|
||||
}
|
||||
|
||||
foreach ($array1 as $key => $value) {
|
||||
$
|
||||
}
|
||||
|
||||
foreach ($array2 as $key => $value) {
|
||||
$
|
||||
}
|
||||
|
||||
foreach ($array3 as $key => $value) {
|
||||
$
|
||||
}
|
||||
|
||||
foreach ($bar->test() as $value) {
|
||||
$
|
||||
}
|
||||
|
||||
foreach ($unknownArray as $member->access => $unknown) {
|
||||
$unkno
|
||||
|
||||
foreach ($loop as $loop) {
|
||||
}
|
||||
|
||||
foreach ($loop->getArray() as $loop) {
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace Whatever;
|
||||
|
||||
use TestNamespace\{TestClass, TestInterface};
|
||||
|
||||
\TestC
|
||||
|
||||
class OtherClass {}
|
|
@ -0,0 +1 @@
|
|||
<
|
|
@ -0,0 +1 @@
|
|||
<
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace MyNamespace;
|
||||
|
||||
class SomeClass
|
||||
{
|
||||
public function someMethod()
|
||||
{
|
||||
tes
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
|
||||
cl
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
class FooClass {
|
||||
public function foo(): FooClass {
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
$fc = new FooClass();
|
||||
$foo = $fc->foo();
|
||||
$foo->
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
|
||||
namespace SomeNamespace {}
|
||||
|
||||
SomeNa
|
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
|
||||
$obj = new ChildClass;
|
||||
$obj->
|
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
|
||||
$obj = new TestClass;
|
||||
$obj->t
|
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
|
||||
TestClass::
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
class FooClass {
|
||||
public static function staticFoo(): FooClass {
|
||||
return new FooClass();
|
||||
}
|
||||
|
||||
public function bar() { }
|
||||
}
|
||||
|
||||
$foo = FooClass::staticFoo();
|
||||
$foo->
|
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
|
||||
TestClass::st
|
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
|
||||
TestClass::$st
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
class ThisClass
|
||||
{
|
||||
private $foo;
|
||||
private $bar;
|
||||
|
||||
protected function method()
|
||||
{
|
||||
}
|
||||
public function test()
|
||||
{
|
||||
$this->
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
class Grand
|
||||
{
|
||||
/** @return $this */
|
||||
public function foo()
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
class Parent1 extends Grand
|
||||
{
|
||||
}
|
||||
|
||||
class Child extends Parent1
|
||||
{
|
||||
public function bar()
|
||||
{
|
||||
$this->foo()->q
|
||||
}
|
||||
public function qux()
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
class ThisClassPrefix extends TestClass
|
||||
{
|
||||
private $foo;
|
||||
private $bar;
|
||||
|
||||
protected function method()
|
||||
{
|
||||
}
|
||||
public function test()
|
||||
{
|
||||
$this->m
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace Whatever;
|
||||
|
||||
use TestNamespace\{TestClass, TestInterface};
|
||||
|
||||
TestC
|
||||
|
||||
class OtherClass {}
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace Whatever;
|
||||
|
||||
use TestNamespace\InnerNamespace as AliasNamespace;
|
||||
|
||||
class IDontShowUpInCompletion {}
|
||||
|
||||
AliasNamespace\I;
|
||||
AliasNamespace\;
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace Whatever;
|
||||
|
||||
use TestNamespace\{TestClass, TestInterface};
|
||||
|
||||
$obj = new
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @param string|null $param A parameter
|
||||
*/
|
||||
function test(string $param = null)
|
||||
{
|
||||
$var = 123;
|
||||
$
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @param string|null $param A parameter
|
||||
*/
|
||||
function test(string $param = null)
|
||||
{
|
||||
$var = 123;
|
||||
$p
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace GlobalFallback;
|
||||
|
||||
// Should fall back to global_symbols.php
|
||||
test_function();
|
||||
echo TEST_CONST;
|
||||
|
||||
// Should not fall back
|
||||
$obj = new TestClass();
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
|
||||
|
||||
$obj = new TestClass();
|
||||
$obj->testMethod();
|
||||
echo $obj->testProperty;
|
||||
TestClass::staticTestMethod();
|
||||
echo TestClass::$staticTestProperty;
|
||||
echo TestClass::TEST_CLASS_CONST;
|
||||
test_function();
|
||||
|
||||
$var = 123;
|
||||
echo $var;
|
||||
|
||||
/**
|
||||
* Aute duis elit reprehenderit tempor cillum proident anim laborum eu laboris reprehenderit ea incididunt.
|
||||
*
|
||||
* @param TestClass $param Adipisicing non non cillum sint incididunt cillum enim mollit.
|
||||
* @return TestClass
|
||||
*/
|
||||
function whatever(TestClass $param): TestClass {
|
||||
echo $param;
|
||||
}
|
||||
|
||||
$fn = function() use ($var) {
|
||||
echo $var;
|
||||
};
|
||||
|
||||
echo TEST_CONST;
|
||||
|
||||
use function test_function;
|
||||
|
||||
if ($abc instanceof TestInterface) {
|
||||
|
||||
}
|
||||
|
||||
// Nested expression
|
||||
$obj->testProperty->testMethod();
|
||||
TestClass::$staticTestProperty[123]->testProperty;
|
||||
|
||||
$child = new ChildClass;
|
||||
echo $child->testMethod();
|
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Esse commodo excepteur pariatur Lorem est aute incididunt reprehenderit.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const TEST_CONST = 123;
|
||||
|
||||
/**
|
||||
* Pariatur ut laborum tempor voluptate consequat ea deserunt.
|
||||
*
|
||||
* Deserunt enim minim sunt sint ea nisi. Deserunt excepteur tempor id nostrud
|
||||
* laboris commodo ad commodo velit mollit qui non officia id. Nulla duis veniam
|
||||
* veniam officia deserunt et non dolore mollit ea quis eiusmod sit non. Occaecat
|
||||
* consequat sunt culpa exercitation pariatur id reprehenderit nisi incididunt Lorem
|
||||
* sint. Officia culpa pariatur laborum nostrud cupidatat consequat mollit.
|
||||
*/
|
||||
class TestClass implements TestInterface
|
||||
{
|
||||
/**
|
||||
* Anim labore veniam consectetur laboris minim quis aute aute esse nulla ad.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const TEST_CLASS_CONST = 123;
|
||||
|
||||
/**
|
||||
* Lorem excepteur officia sit anim velit veniam enim.
|
||||
*
|
||||
* @var TestClass[]
|
||||
*/
|
||||
public static $staticTestProperty;
|
||||
|
||||
/**
|
||||
* Reprehenderit magna velit mollit ipsum do.
|
||||
*
|
||||
* @var TestClass
|
||||
*/
|
||||
public $testProperty;
|
||||
|
||||
/**
|
||||
* Do magna consequat veniam minim proident eiusmod incididunt aute proident.
|
||||
*/
|
||||
public static function staticTestMethod()
|
||||
{
|
||||
echo self::TEST_CLASS_CONST;
|
||||
}
|
||||
|
||||
/**
|
||||
* Non culpa nostrud mollit esse sunt laboris in irure ullamco cupidatat amet.
|
||||
*
|
||||
* @param TestClass $testParameter Lorem sunt velit incididunt mollit
|
||||
* @return TestClass
|
||||
*/
|
||||
public function testMethod($testParameter): TestInterface
|
||||
{
|
||||
$this->testProperty = $testParameter;
|
||||
}
|
||||
}
|
||||
|
||||
trait TestTrait
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
interface TestInterface
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Officia aliquip adipisicing et nulla et laboris dolore labore.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function test_function()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
new class {
|
||||
const TEST_CLASS_CONST = 123;
|
||||
public static $staticTestProperty;
|
||||
public $testProperty;
|
||||
|
||||
public static function staticTestMethod()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function testMethod($testParameter)
|
||||
{
|
||||
$testVariable = 123;
|
||||
}
|
||||
};
|
||||
|
||||
class ChildClass extends TestClass {}
|
||||
|
||||
/**
|
||||
* Lorem ipsum dolor sit amet, consectetur.
|
||||
*/
|
||||
define('TEST_DEFINE_CONSTANT', false);
|
||||
|
||||
print TEST_DEFINE_CONSTANT ? 'true' : 'false';
|
||||
|
||||
/**
|
||||
* Neither this class nor its members are referenced anywhere
|
||||
*/
|
||||
class UnusedClass
|
||||
{
|
||||
public $unusedProperty;
|
||||
|
||||
public function unusedMethod()
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
|
||||
interface class
|
||||
{
|
||||
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
|
||||
echo "Hello";
|
||||
|
||||
namespace A;
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
namespace TestNamespace;
|
||||
|
||||
$obj = new TestClass($a, $b, $c);
|
||||
$obj->testMethod();
|
||||
echo $obj->testProperty;
|
||||
TestClass::staticTestMethod();
|
||||
echo TestClass::$staticTestProperty;
|
||||
echo TestClass::TEST_CLASS_CONST;
|
||||
test_function();
|
||||
|
||||
$var = 123;
|
||||
echo $var;
|
||||
|
||||
/**
|
||||
* Aute duis elit reprehenderit tempor cillum proident anim laborum eu laboris reprehenderit ea incididunt.
|
||||
*
|
||||
* @param TestClass $param Adipisicing non non cillum sint incididunt cillum enim mollit.
|
||||
* @return TestClass
|
||||
*/
|
||||
function whatever(TestClass $param): TestClass {
|
||||
echo $param;
|
||||
}
|
||||
|
||||
$fn = function() use ($var) {
|
||||
echo $var;
|
||||
};
|
||||
|
||||
echo TEST_CONST;
|
||||
|
||||
use function TestNamespace\test_function;
|
||||
|
||||
if ($abc instanceof TestInterface) {
|
||||
|
||||
}
|
||||
|
||||
// Nested expressions
|
||||
$obj->testProperty->testMethod();
|
||||
TestClass::$staticTestProperty[123]->testProperty;
|
||||
|
||||
$child = new ChildClass;
|
||||
echo $child->testMethod();
|
|
@ -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();
|
|
@ -0,0 +1,110 @@
|
|||
<?php
|
||||
|
||||
namespace TestNamespace;
|
||||
|
||||
/**
|
||||
* Esse commodo excepteur pariatur Lorem est aute incididunt reprehenderit.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const TEST_CONST = 123;
|
||||
|
||||
/**
|
||||
* Pariatur ut laborum tempor voluptate consequat ea deserunt.
|
||||
*
|
||||
* Deserunt enim minim sunt sint ea nisi. Deserunt excepteur tempor id nostrud
|
||||
* laboris commodo ad commodo velit mollit qui non officia id. Nulla duis veniam
|
||||
* veniam officia deserunt et non dolore mollit ea quis eiusmod sit non. Occaecat
|
||||
* consequat sunt culpa exercitation pariatur id reprehenderit nisi incididunt Lorem
|
||||
* sint. Officia culpa pariatur laborum nostrud cupidatat consequat mollit.
|
||||
*/
|
||||
class TestClass implements TestInterface
|
||||
{
|
||||
/**
|
||||
* Anim labore veniam consectetur laboris minim quis aute aute esse nulla ad.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const TEST_CLASS_CONST = 123;
|
||||
|
||||
/**
|
||||
* Lorem excepteur officia sit anim velit veniam enim.
|
||||
*
|
||||
* @var TestClass[]
|
||||
*/
|
||||
public static $staticTestProperty;
|
||||
|
||||
/**
|
||||
* Reprehenderit magna velit mollit ipsum do.
|
||||
*
|
||||
* @var TestClass
|
||||
*/
|
||||
public $testProperty;
|
||||
|
||||
/**
|
||||
* Do magna consequat veniam minim proident eiusmod incididunt aute proident.
|
||||
*/
|
||||
public static function staticTestMethod()
|
||||
{
|
||||
echo self::TEST_CLASS_CONST;
|
||||
}
|
||||
|
||||
/**
|
||||
* Non culpa nostrud mollit esse sunt laboris in irure ullamco cupidatat amet.
|
||||
*
|
||||
* @param TestClass $testParameter Lorem sunt velit incididunt mollit
|
||||
* @return TestClass
|
||||
*/
|
||||
public function testMethod($testParameter): TestInterface
|
||||
{
|
||||
$this->testProperty = $testParameter;
|
||||
}
|
||||
}
|
||||
|
||||
trait TestTrait
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
interface TestInterface
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Officia aliquip adipisicing et nulla et laboris dolore labore.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function test_function()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
new class {
|
||||
const TEST_CLASS_CONST = 123;
|
||||
public static $staticTestProperty;
|
||||
public $testProperty;
|
||||
|
||||
public static function staticTestMethod()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function testMethod($testParameter)
|
||||
{
|
||||
$testVariable = 123;
|
||||
}
|
||||
};
|
||||
|
||||
class ChildClass extends TestClass {}
|
||||
|
||||
class Example {
|
||||
public function __construct() {}
|
||||
public function __destruct() {}
|
||||
}
|
||||
|
||||
namespace TestNamespace\InnerNamespace;
|
||||
|
||||
class InnerClass {
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace SecondTestNamespace;
|
||||
|
||||
use TestNamespace\TestClass;
|
||||
use TestNamespace\{TestTrait, TestInterface};
|
After Width: | Height: | Size: 158 KiB |
After Width: | Height: | Size: 95 KiB |
After Width: | Height: | Size: 206 KiB |
After Width: | Height: | Size: 47 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 63 KiB |
After Width: | Height: | Size: 316 KiB |
After Width: | Height: | Size: 250 KiB |
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
|
||||
# php.ini for Docker
|
||||
|
||||
error_reporting = E_ALL
|
||||
display_errors = stderr
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0"?>
|
||||
<ruleset name="PHP Language Server">
|
||||
<file>src</file>
|
||||
<file>tests</file>
|
||||
<exclude-pattern>tests/Validation/cases</exclude-pattern>
|
||||
<rule ref="PSR2">
|
||||
<exclude name="PSR2.Namespaces.UseDeclaration.MultipleDeclarations"/>
|
||||
<exclude name="PSR2.ControlStructures.ElseIfDeclaration.NotAllowed"/>
|
||||
<exclude name="PSR2.ControlStructures.ControlStructureSpacing.SpacingAfterOpenBrace"/>
|
||||
<exclude name="Squiz.WhiteSpace.ControlStructureSpacing.SpacingBeforeClose"/>
|
||||
<exclude name="Squiz.WhiteSpace.ControlStructureSpacing.SpacingAfterOpen"/>
|
||||
</rule>
|
||||
</ruleset>
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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 suffix="Test.php">./tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<filter>
|
||||
<whitelist processUncoveredFilesFromWhitelist="true">
|
||||
<directory suffix=".php">./src</directory>
|
||||
</whitelist>
|
||||
</filter>
|
||||
<php>
|
||||
<ini name="memory_limit" value="1024M"/>
|
||||
</php>
|
||||
</phpunit>
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\Cache;
|
||||
|
||||
use Sabre\Event\Promise;
|
||||
|
||||
/**
|
||||
* A key/value store for caching purposes
|
||||
*/
|
||||
interface Cache
|
||||
{
|
||||
/**
|
||||
* Gets a value from the cache
|
||||
*
|
||||
* @param string $key
|
||||
* @return Promise <mixed>
|
||||
*/
|
||||
public function get(string $key): Promise;
|
||||
|
||||
/**
|
||||
* Sets a value in the cache
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @return Promise
|
||||
*/
|
||||
public function set(string $key, $value): Promise;
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\Cache;
|
||||
|
||||
use LanguageServer\LanguageClient;
|
||||
use Sabre\Event\Promise;
|
||||
|
||||
/**
|
||||
* Caches content through a xcache/* requests
|
||||
*/
|
||||
class ClientCache implements Cache
|
||||
{
|
||||
/**
|
||||
* @var LanguageClient
|
||||
*/
|
||||
public $client;
|
||||
|
||||
/**
|
||||
* @param LanguageClient $client
|
||||
*/
|
||||
public function __construct(LanguageClient $client)
|
||||
{
|
||||
$this->client = $client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a value from the cache
|
||||
*
|
||||
* @param string $key
|
||||
* @return Promise <mixed>
|
||||
*/
|
||||
public function get(string $key): Promise
|
||||
{
|
||||
return $this->client->xcache->get($key)->then('unserialize')->otherwise(function () {
|
||||
// Ignore
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a value in the cache
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @return Promise
|
||||
*/
|
||||
public function set(string $key, $value): Promise
|
||||
{
|
||||
return $this->client->xcache->set($key, serialize($value))->otherwise(function () {
|
||||
// Ignore
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\Cache;
|
||||
|
||||
use Sabre\Event\Promise;
|
||||
|
||||
/**
|
||||
* Caches content on the file system
|
||||
*/
|
||||
class FileSystemCache implements Cache
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $cacheDir;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if (strtoupper(substr(php_uname('s'), 0, 3)) === 'WIN') {
|
||||
$this->cacheDir = getenv('LOCALAPPDATA') . '\\PHP Language Server\\';
|
||||
} else if (getenv('XDG_CACHE_HOME')) {
|
||||
$this->cacheDir = getenv('XDG_CACHE_HOME') . '/phpls/';
|
||||
} else {
|
||||
$this->cacheDir = getenv('HOME') . '/.phpls/';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a value from the cache
|
||||
*
|
||||
* @param string $key
|
||||
* @return Promise <mixed>
|
||||
*/
|
||||
public function get(string $key): Promise
|
||||
{
|
||||
try {
|
||||
$file = $this->cacheDir . urlencode($key);
|
||||
if (!file_exists($file)) {
|
||||
return Promise\resolve(null);
|
||||
}
|
||||
return Promise\resolve(unserialize(file_get_contents($file)));
|
||||
} catch (\Exception $e) {
|
||||
return Promise\resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a value in the cache
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @return Promise
|
||||
*/
|
||||
public function set(string $key, $value): Promise
|
||||
{
|
||||
try {
|
||||
$file = $this->cacheDir . urlencode($key);
|
||||
if (!file_exists($this->cacheDir)) {
|
||||
mkdir($this->cacheDir);
|
||||
}
|
||||
file_put_contents($file, serialize($value));
|
||||
} finally {
|
||||
return Promise\resolve(null);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\Client;
|
||||
|
||||
use LanguageServer\ClientHandler;
|
||||
use LanguageServerProtocol\{Diagnostic, TextDocumentItem, TextDocumentIdentifier};
|
||||
use Sabre\Event\Promise;
|
||||
use JsonMapper;
|
||||
|
||||
/**
|
||||
* Provides method handlers for all textDocument/* methods
|
||||
*/
|
||||
class TextDocument
|
||||
{
|
||||
/**
|
||||
* @var ClientHandler
|
||||
*/
|
||||
private $handler;
|
||||
|
||||
/**
|
||||
* @var JsonMapper
|
||||
*/
|
||||
private $mapper;
|
||||
|
||||
public function __construct(ClientHandler $handler, JsonMapper $mapper)
|
||||
{
|
||||
$this->handler = $handler;
|
||||
$this->mapper = $mapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Diagnostics notification are sent from the server to the client to signal results of validation runs.
|
||||
*
|
||||
* @param string $uri
|
||||
* @param Diagnostic[] $diagnostics
|
||||
* @return Promise <void>
|
||||
*/
|
||||
public function publishDiagnostics(string $uri, array $diagnostics): Promise
|
||||
{
|
||||
return $this->handler->notify('textDocument/publishDiagnostics', [
|
||||
'uri' => $uri,
|
||||
'diagnostics' => $diagnostics
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The content request is sent from a server to a client
|
||||
* to request the current content of a text document identified by the URI
|
||||
*
|
||||
* @param TextDocumentIdentifier $textDocument The document to get the content for
|
||||
* @return Promise <TextDocumentItem> The document's current content
|
||||
*/
|
||||
public function xcontent(TextDocumentIdentifier $textDocument): Promise
|
||||
{
|
||||
return $this->handler->request(
|
||||
'textDocument/xcontent',
|
||||
['textDocument' => $textDocument]
|
||||
)->then(function ($result) {
|
||||
return $this->mapper->map($result, new TextDocumentItem);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\Client;
|
||||
|
||||
use LanguageServer\ClientHandler;
|
||||
use Sabre\Event\Promise;
|
||||
|
||||
/**
|
||||
* Provides method handlers for all window/* methods
|
||||
*/
|
||||
class Window
|
||||
{
|
||||
/**
|
||||
* @var ClientHandler
|
||||
*/
|
||||
private $handler;
|
||||
|
||||
public function __construct(ClientHandler $handler)
|
||||
{
|
||||
$this->handler = $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* The show message notification is sent from a server to a client
|
||||
* to ask the client to display a particular message in the user interface.
|
||||
*
|
||||
* @param int $type
|
||||
* @param string $message
|
||||
* @return Promise <void>
|
||||
*/
|
||||
public function showMessage(int $type, string $message): Promise
|
||||
{
|
||||
return $this->handler->notify('window/showMessage', ['type' => $type, 'message' => $message]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The log message notification is sent from the server to the client to ask the client to log a particular message.
|
||||
*
|
||||
* @param int $type
|
||||
* @param string $message
|
||||
* @return Promise <void>
|
||||
*/
|
||||
public function logMessage(int $type, string $message): Promise
|
||||
{
|
||||
return $this->handler->notify('window/logMessage', ['type' => $type, 'message' => $message]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\Client;
|
||||
|
||||
use LanguageServer\ClientHandler;
|
||||
use LanguageServerProtocol\TextDocumentIdentifier;
|
||||
use Sabre\Event\Promise;
|
||||
use JsonMapper;
|
||||
|
||||
/**
|
||||
* Provides method handlers for all workspace/* methods
|
||||
*/
|
||||
class Workspace
|
||||
{
|
||||
/**
|
||||
* @var ClientHandler
|
||||
*/
|
||||
private $handler;
|
||||
|
||||
/**
|
||||
* @var JsonMapper
|
||||
*/
|
||||
private $mapper;
|
||||
|
||||
public function __construct(ClientHandler $handler, JsonMapper $mapper)
|
||||
{
|
||||
$this->handler = $handler;
|
||||
$this->mapper = $mapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all files in a directory
|
||||
*
|
||||
* @param string $base The base directory (defaults to the workspace)
|
||||
* @return Promise <TextDocumentIdentifier[]> Array of documents
|
||||
*/
|
||||
public function xfiles(string $base = null): Promise
|
||||
{
|
||||
return $this->handler->request(
|
||||
'workspace/xfiles',
|
||||
['base' => $base]
|
||||
)->then(function (array $textDocuments) {
|
||||
return $this->mapper->mapArray($textDocuments, [], TextDocumentIdentifier::class);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\Client;
|
||||
|
||||
use LanguageServer\ClientHandler;
|
||||
use Sabre\Event\Promise;
|
||||
|
||||
/**
|
||||
* Provides method handlers for all xcache/* methods
|
||||
*/
|
||||
class XCache
|
||||
{
|
||||
/**
|
||||
* @var ClientHandler
|
||||
*/
|
||||
private $handler;
|
||||
|
||||
public function __construct(ClientHandler $handler)
|
||||
{
|
||||
$this->handler = $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return Promise <mixed>
|
||||
*/
|
||||
public function get(string $key): Promise
|
||||
{
|
||||
return $this->handler->request('xcache/get', ['key' => $key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @return Promise <mixed>
|
||||
*/
|
||||
public function set(string $key, $value): Promise
|
||||
{
|
||||
return $this->handler->notify('xcache/set', ['key' => $key, 'value' => $value]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer;
|
||||
|
||||
use AdvancedJsonRpc;
|
||||
use Sabre\Event\Promise;
|
||||
|
||||
class ClientHandler
|
||||
{
|
||||
/**
|
||||
* @var ProtocolReader
|
||||
*/
|
||||
public $protocolReader;
|
||||
|
||||
/**
|
||||
* @var ProtocolWriter
|
||||
*/
|
||||
public $protocolWriter;
|
||||
|
||||
/**
|
||||
* @var IdGenerator
|
||||
*/
|
||||
public $idGenerator;
|
||||
|
||||
public function __construct(ProtocolReader $protocolReader, ProtocolWriter $protocolWriter)
|
||||
{
|
||||
$this->protocolReader = $protocolReader;
|
||||
$this->protocolWriter = $protocolWriter;
|
||||
$this->idGenerator = new IdGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the client and returns a promise that is resolved with the result or rejected with the error
|
||||
*
|
||||
* @param string $method The method to call
|
||||
* @param array|object $params The method parameters
|
||||
* @return Promise <mixed> Resolved with the result of the request or rejected with an error
|
||||
*/
|
||||
public function request(string $method, $params): Promise
|
||||
{
|
||||
$id = $this->idGenerator->generate();
|
||||
return $this->protocolWriter->write(
|
||||
new Message(
|
||||
new AdvancedJsonRpc\Request($id, $method, (object)$params)
|
||||
)
|
||||
)->then(function () use ($id) {
|
||||
$promise = new Promise;
|
||||
$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);
|
||||
if (AdvancedJsonRpc\SuccessResponse::isSuccessResponse($msg->body)) {
|
||||
$promise->fulfill($msg->body->result);
|
||||
} else {
|
||||
$promise->reject($msg->body->error);
|
||||
}
|
||||
}
|
||||
};
|
||||
$this->protocolReader->on('message', $listener);
|
||||
return $promise;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a notification to the client
|
||||
*
|
||||
* @param string $method The method to call
|
||||
* @param array|object $params The method parameters
|
||||
* @return Promise <null> Will be resolved as soon as the notification has been sent
|
||||
*/
|
||||
public function notify(string $method, $params): Promise
|
||||
{
|
||||
return $this->protocolWriter->write(
|
||||
new Message(
|
||||
new AdvancedJsonRpc\Notification($method, (object)$params)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer;
|
||||
|
||||
use PhpParser\{NodeVisitorAbstract, Node};
|
||||
|
||||
class ColumnCalculator extends NodeVisitorAbstract
|
||||
{
|
||||
private $code;
|
||||
private $codeLength;
|
||||
|
||||
public function __construct($code)
|
||||
{
|
||||
$this->code = $code;
|
||||
$this->codeLength = strlen($code);
|
||||
}
|
||||
|
||||
public function enterNode(Node $node)
|
||||
{
|
||||
$startFilePos = $node->getAttribute('startFilePos');
|
||||
$endFilePos = $node->getAttribute('endFilePos');
|
||||
|
||||
if ($startFilePos > $this->codeLength || $endFilePos > $this->codeLength) {
|
||||
throw new \RuntimeException('Invalid position information');
|
||||
}
|
||||
|
||||
$startLinePos = strrpos($this->code, "\n", $startFilePos - $this->codeLength);
|
||||
if ($startLinePos === false) {
|
||||
$startLinePos = -1;
|
||||
}
|
||||
|
||||
$endLinePos = strrpos($this->code, "\n", $endFilePos - $this->codeLength);
|
||||
if ($endLinePos === false) {
|
||||
$endLinePos = -1;
|
||||
}
|
||||
|
||||
$node->setAttribute('startColumn', $startFilePos - $startLinePos);
|
||||
$node->setAttribute('endColumn', $endFilePos - $endLinePos);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,707 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer;
|
||||
|
||||
use LanguageServer\Index\ReadableIndex;
|
||||
use LanguageServer\Factory\CompletionItemFactory;
|
||||
use LanguageServerProtocol\{
|
||||
TextEdit,
|
||||
Range,
|
||||
Position,
|
||||
CompletionList,
|
||||
CompletionItem,
|
||||
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
|
||||
{
|
||||
const KEYWORDS = [
|
||||
'?>',
|
||||
'__halt_compiler',
|
||||
'abstract',
|
||||
'and',
|
||||
'array',
|
||||
'as',
|
||||
'break',
|
||||
'callable',
|
||||
'case',
|
||||
'catch',
|
||||
'class',
|
||||
'clone',
|
||||
'const',
|
||||
'continue',
|
||||
'declare',
|
||||
'default',
|
||||
'die',
|
||||
'do',
|
||||
'echo',
|
||||
'else',
|
||||
'elseif',
|
||||
'empty',
|
||||
'enddeclare',
|
||||
'endfor',
|
||||
'endforeach',
|
||||
'endif',
|
||||
'endswitch',
|
||||
'endwhile',
|
||||
'eval',
|
||||
'exit',
|
||||
'extends',
|
||||
'false',
|
||||
'final',
|
||||
'finally',
|
||||
'for',
|
||||
'foreach',
|
||||
'function',
|
||||
'global',
|
||||
'goto',
|
||||
'if',
|
||||
'implements',
|
||||
'include',
|
||||
'include_once',
|
||||
'instanceof',
|
||||
'insteadof',
|
||||
'interface',
|
||||
'isset',
|
||||
'list',
|
||||
'namespace',
|
||||
'new',
|
||||
'null',
|
||||
'or',
|
||||
'print',
|
||||
'private',
|
||||
'protected',
|
||||
'public',
|
||||
'require',
|
||||
'require_once',
|
||||
'return',
|
||||
'static',
|
||||
'switch',
|
||||
'throw',
|
||||
'trait',
|
||||
'true',
|
||||
'try',
|
||||
'unset',
|
||||
'use',
|
||||
'var',
|
||||
'while',
|
||||
'xor',
|
||||
'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')
|
||||
];
|
||||
|
||||
/**
|
||||
* @var DefinitionResolver
|
||||
*/
|
||||
private $definitionResolver;
|
||||
|
||||
/**
|
||||
* @var Project
|
||||
*/
|
||||
private $project;
|
||||
|
||||
/**
|
||||
* @var ReadableIndex
|
||||
*/
|
||||
private $index;
|
||||
|
||||
/**
|
||||
* @param DefinitionResolver $definitionResolver
|
||||
* @param ReadableIndex $index
|
||||
*/
|
||||
public function __construct(DefinitionResolver $definitionResolver, ReadableIndex $index)
|
||||
{
|
||||
$this->definitionResolver = $definitionResolver;
|
||||
$this->index = $index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns suggestions for a specific cursor position in a document
|
||||
*
|
||||
* @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,
|
||||
CompletionContext $context = null
|
||||
): CompletionList {
|
||||
// This can be made much more performant if the tree follows specific invariants.
|
||||
$node = $doc->getNodeAtPosition($pos);
|
||||
|
||||
// Get the node at the position under the cursor
|
||||
$offset = $node === null ? -1 : $pos->toOffset($node->getFileContents());
|
||||
if (
|
||||
$node !== null
|
||||
&& $offset > $node->getEndPosition()
|
||||
&& $node->parent !== null
|
||||
&& $node->parent->getLastChild() instanceof PhpParser\MissingToken
|
||||
) {
|
||||
$node = $node->parent;
|
||||
}
|
||||
|
||||
$list = new CompletionList;
|
||||
$list->isIncomplete = true;
|
||||
|
||||
if ($node instanceof Node\Expression\Variable &&
|
||||
$node->parent instanceof Node\Expression\ObjectCreationExpression &&
|
||||
$node->name instanceof PhpParser\MissingToken
|
||||
) {
|
||||
$node = $node->parent;
|
||||
}
|
||||
|
||||
// Inspect the type of expression under the cursor
|
||||
|
||||
$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
|
||||
$item = new CompletionItem('<?php', CompletionItemKind::KEYWORD);
|
||||
$item->textEdit = new TextEdit(
|
||||
new Range($pos, $pos),
|
||||
stripStringOverlap($doc->getRange(new Range(new Position(0, 0), $pos)), '<?php')
|
||||
);
|
||||
$list->items[] = $item;
|
||||
|
||||
} elseif (
|
||||
$node instanceof Node\Expression\Variable
|
||||
&& !(
|
||||
$node->parent instanceof Node\Expression\ScopedPropertyAccessExpression
|
||||
&& $node->parent->memberName === $node
|
||||
)
|
||||
) {
|
||||
// Variables
|
||||
//
|
||||
// $|
|
||||
// $a|
|
||||
|
||||
// Find variables, parameters and use statements in the scope
|
||||
$namePrefix = $node->getName() ?? '';
|
||||
foreach ($this->suggestVariablesAtNode($node, $namePrefix) as $var) {
|
||||
$item = new CompletionItem;
|
||||
$item->kind = CompletionItemKind::VARIABLE;
|
||||
$item->label = '$' . $var->getName();
|
||||
$item->documentation = $this->definitionResolver->getDocumentationFromNode($var);
|
||||
$item->detail = (string)$this->definitionResolver->getTypeFromNode($var);
|
||||
$item->textEdit = new TextEdit(
|
||||
new Range($pos, $pos),
|
||||
stripStringOverlap($doc->getRange(new Range(new Position(0, 0), $pos)), $item->label)
|
||||
);
|
||||
$list->items[] = $item;
|
||||
}
|
||||
|
||||
} elseif ($node instanceof Node\Expression\MemberAccessExpression) {
|
||||
// Member access expressions
|
||||
//
|
||||
// $a->c|
|
||||
// $a->|
|
||||
|
||||
// Multiple prefixes for all possible types
|
||||
$fqns = FqnUtilities\getFqnsFromType(
|
||||
$this->definitionResolver->resolveExpressionNodeToType($node->dereferencableExpression)
|
||||
);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} elseif (
|
||||
($scoped = $node->parent) instanceof Node\Expression\ScopedPropertyAccessExpression ||
|
||||
($scoped = $node) instanceof Node\Expression\ScopedPropertyAccessExpression
|
||||
) {
|
||||
// Static class members and constants
|
||||
//
|
||||
// A\B\C::$a|
|
||||
// A\B\C::|
|
||||
// A\B\C::$|
|
||||
// A\B\C::foo|
|
||||
//
|
||||
// TODO: $a::|
|
||||
|
||||
// Resolve all possible types to FQNs
|
||||
$fqns = FqnUtilities\getFqnsFromType(
|
||||
$classType = $this->definitionResolver->resolveExpressionNodeToType($scoped->scopeResolutionQualifier)
|
||||
);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} elseif (
|
||||
ParserHelpers\isConstantFetch($node)
|
||||
// Creation gets set in case of an instantiation (`new` expression)
|
||||
|| ($creation = $node->parent) instanceof Node\Expression\ObjectCreationExpression
|
||||
|| (($creation = $node) instanceof Node\Expression\ObjectCreationExpression)
|
||||
) {
|
||||
// Class instantiations, function calls, constant fetches, class names
|
||||
//
|
||||
// new MyCl|
|
||||
// my_func|
|
||||
// MY_CONS|
|
||||
// MyCla|
|
||||
// \MyCla|
|
||||
|
||||
// The name Node under the cursor
|
||||
$nameNode = isset($creation) ? $creation->classTypeDesignator : $node;
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
$namespaceNode = $node->getNamespaceDefinition();
|
||||
/** @var string The current namespace without a leading backslash. */
|
||||
$currentNamespace = $namespaceNode === null ? '' : $namespaceNode->name->getText();
|
||||
|
||||
/** @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();
|
||||
}
|
||||
|
||||
/** @var bool Whether we are in a new expression */
|
||||
$isCreation = isset($creation);
|
||||
|
||||
/** @var array Import (use) tables */
|
||||
$importTables = $node->getImportTablesForCurrentScope();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Generator
|
||||
*/
|
||||
private function expandParentFqns(array $fqns) : Generator
|
||||
{
|
||||
foreach ($fqns as $fqn) {
|
||||
yield $fqn;
|
||||
$def = $this->index->getDefinition($fqn);
|
||||
if ($def !== null) {
|
||||
foreach ($def->getAncestorDefinitions($this->index) as $name => $def) {
|
||||
yield $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will walk the AST upwards until a function-like node is met
|
||||
* and at each level walk all previous siblings and their children to search for definitions
|
||||
* of that variable
|
||||
*
|
||||
* @param Node $node
|
||||
* @param string $namePrefix Prefix to filter
|
||||
* @return array <Node\Expr\Variable|Node\Param|Node\Expr\ClosureUse>
|
||||
*/
|
||||
private function suggestVariablesAtNode(Node $node, string $namePrefix = ''): array
|
||||
{
|
||||
$vars = [];
|
||||
|
||||
// Find variables in the node itself
|
||||
// When getting completion in the middle of a function, $node will be the function node
|
||||
// so we need to search it
|
||||
foreach ($this->findVariableDefinitionsInNode($node, $namePrefix) as $var) {
|
||||
// Only use the first definition
|
||||
if (!isset($vars[$var->name])) {
|
||||
$vars[$var->name] = $var;
|
||||
}
|
||||
}
|
||||
|
||||
// Walk the AST upwards until a scope boundary is met
|
||||
$level = $node;
|
||||
while ($level && !($level instanceof PhpParser\FunctionLike)) {
|
||||
// Walk siblings before the node
|
||||
$sibling = $level;
|
||||
while ($sibling = $sibling->getPreviousSibling()) {
|
||||
// Collect all variables inside the sibling node
|
||||
foreach ($this->findVariableDefinitionsInNode($sibling, $namePrefix) as $var) {
|
||||
$vars[$var->getName()] = $var;
|
||||
}
|
||||
}
|
||||
$level = $level->parent;
|
||||
}
|
||||
|
||||
// If the traversal ended because a function was met,
|
||||
// also add its parameters and closure uses to the result list
|
||||
if ($level && $level instanceof PhpParser\FunctionLike && $level->parameters !== null) {
|
||||
foreach ($level->parameters->getValues() as $param) {
|
||||
$paramName = $param->getName();
|
||||
if (empty($namePrefix) || strpos($paramName, $namePrefix) !== false) {
|
||||
$vars[$paramName] = $param;
|
||||
}
|
||||
}
|
||||
|
||||
if ($level instanceof Node\Expression\AnonymousFunctionCreationExpression
|
||||
&& $level->anonymousFunctionUseClause !== null
|
||||
&& $level->anonymousFunctionUseClause->useVariableNameList !== null) {
|
||||
foreach ($level->anonymousFunctionUseClause->useVariableNameList->getValues() as $use) {
|
||||
$useName = $use->getName();
|
||||
if (empty($namePrefix) || strpos($useName, $namePrefix) !== false) {
|
||||
$vars[$useName] = $use;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($vars);
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches the subnodes of a node for variable assignments
|
||||
*
|
||||
* @param Node $node
|
||||
* @param string $namePrefix Prefix to filter
|
||||
* @return Node\Expression\Variable[]
|
||||
*/
|
||||
private function findVariableDefinitionsInNode(Node $node, string $namePrefix = ''): array
|
||||
{
|
||||
$vars = [];
|
||||
// If the child node is a variable assignment, save it
|
||||
|
||||
$isAssignmentToVariable = function ($node) {
|
||||
return $node instanceof Node\Expression\AssignmentExpression;
|
||||
};
|
||||
|
||||
if ($this->isAssignmentToVariableWithPrefix($node, $namePrefix)) {
|
||||
$vars[] = $node->leftOperand;
|
||||
} elseif ($node instanceof Node\ForeachKey || $node instanceof Node\ForeachValue) {
|
||||
foreach ($node->getDescendantNodes() as $descendantNode) {
|
||||
if ($descendantNode instanceof Node\Expression\Variable
|
||||
&& ($namePrefix === '' || strpos($descendantNode->getName(), $namePrefix) !== false)
|
||||
) {
|
||||
$vars[] = $descendantNode;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Get all descendent variables, then filter to ones that start with $namePrefix.
|
||||
// Avoiding closure usage in tight loop
|
||||
foreach ($node->getDescendantNodes($isAssignmentToVariable) as $descendantNode) {
|
||||
if ($this->isAssignmentToVariableWithPrefix($descendantNode, $namePrefix)) {
|
||||
$vars[] = $descendantNode->leftOperand;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $vars;
|
||||
}
|
||||
|
||||
private function isAssignmentToVariableWithPrefix(Node $node, string $namePrefix): bool
|
||||
{
|
||||
return $node instanceof Node\Expression\AssignmentExpression
|
||||
&& $node->leftOperand instanceof Node\Expression\Variable
|
||||
&& ($namePrefix === '' || strpos($node->leftOperand->getName(), $namePrefix) !== false);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer;
|
||||
|
||||
use LanguageServer\FilesFinder\FileSystemFilesFinder;
|
||||
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
|
||||
use LanguageServer\Index\StubsIndex;
|
||||
use phpDocumentor\Reflection\DocBlockFactory;
|
||||
use Webmozart\PathUtil\Path;
|
||||
use Sabre\Uri;
|
||||
use function Sabre\Event\coroutine;
|
||||
use Microsoft\PhpParser;
|
||||
|
||||
foreach ([__DIR__ . '/../../../autoload.php', __DIR__ . '/../autoload.php', __DIR__ . '/../vendor/autoload.php'] as $file) {
|
||||
if (file_exists($file)) {
|
||||
require $file;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
class ComposerScripts
|
||||
{
|
||||
public static function parseStubs()
|
||||
{
|
||||
coroutine(function () {
|
||||
|
||||
$index = new StubsIndex;
|
||||
|
||||
$finder = new FileSystemFilesFinder;
|
||||
$contentRetriever = new FileSystemContentRetriever;
|
||||
$docBlockFactory = DocBlockFactory::createInstance();
|
||||
$parser = new PhpParser\Parser();
|
||||
$definitionResolver = new DefinitionResolver($index);
|
||||
|
||||
$stubsLocation = null;
|
||||
foreach ([__DIR__ . '/../../../jetbrains/phpstorm-stubs', __DIR__ . '/../vendor/jetbrains/phpstorm-stubs'] as $dir) {
|
||||
if (file_exists($dir)) {
|
||||
$stubsLocation = Path::canonicalize($dir);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$stubsLocation) {
|
||||
throw new \Exception('jetbrains/phpstorm-stubs package not found');
|
||||
}
|
||||
|
||||
$uris = yield $finder->find("$stubsLocation/**/*.php");
|
||||
|
||||
foreach ($uris as $uri) {
|
||||
echo "Parsing $uri\n";
|
||||
$content = yield $contentRetriever->retrieve($uri);
|
||||
|
||||
// Change URI to phpstubs://
|
||||
$parts = Uri\parse($uri);
|
||||
$parts['path'] = Path::makeRelative($parts['path'], $stubsLocation);
|
||||
$parts['scheme'] = 'phpstubs';
|
||||
$uri = Uri\build($parts);
|
||||
|
||||
// Create a new document and add it to $index
|
||||
new PhpDocument($uri, $content, $index, $parser, $docBlockFactory, $definitionResolver);
|
||||
}
|
||||
|
||||
$index->setComplete();
|
||||
|
||||
echo "Saving Index\n";
|
||||
|
||||
$index->save();
|
||||
|
||||
echo "Finished\n";
|
||||
})->wait();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\ContentRetriever;
|
||||
|
||||
use LanguageServer\LanguageClient;
|
||||
use LanguageServerProtocol\{TextDocumentIdentifier, TextDocumentItem};
|
||||
use Sabre\Event\Promise;
|
||||
|
||||
/**
|
||||
* Retrieves file content from the client through a textDocument/xcontent request
|
||||
*/
|
||||
class ClientContentRetriever implements ContentRetriever
|
||||
{
|
||||
/**
|
||||
* @param LanguageClient $client
|
||||
*/
|
||||
public function __construct(LanguageClient $client)
|
||||
{
|
||||
$this->client = $client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the content of a text document identified by the URI through a textDocument/xcontent request
|
||||
*
|
||||
* @param string $uri The URI of the document
|
||||
* @return Promise <string> Resolved with the content as a string
|
||||
*/
|
||||
public function retrieve(string $uri): Promise
|
||||
{
|
||||
return $this->client->textDocument->xcontent(new TextDocumentIdentifier($uri))
|
||||
->then(function (TextDocumentItem $textDocument) {
|
||||
return $textDocument->text;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\ContentRetriever;
|
||||
|
||||
use Sabre\Event\Promise;
|
||||
|
||||
/**
|
||||
* Interface for retrieving the content of a text document
|
||||
*/
|
||||
interface ContentRetriever
|
||||
{
|
||||
/**
|
||||
* Retrieves the content of a text document identified by the URI
|
||||
*
|
||||
* @param string $uri The URI of the document
|
||||
* @return Promise <string> Resolved with the content as a string
|
||||
*/
|
||||
public function retrieve(string $uri): Promise;
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\ContentRetriever;
|
||||
|
||||
use Sabre\Event\Promise;
|
||||
use function LanguageServer\uriToPath;
|
||||
|
||||
/**
|
||||
* Retrieves document content from the file system
|
||||
*/
|
||||
class FileSystemContentRetriever implements ContentRetriever
|
||||
{
|
||||
/**
|
||||
* Retrieves the content of a text document identified by the URI from the file system
|
||||
*
|
||||
* @param string $uri The URI of the document
|
||||
* @return Promise <string> Resolved with the content as a string
|
||||
*/
|
||||
public function retrieve(string $uri): Promise
|
||||
{
|
||||
return Promise\resolve(file_get_contents(uriToPath($uri)));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer;
|
||||
|
||||
/**
|
||||
* Thrown when the document content is not parsed because it exceeds the size limit
|
||||
*/
|
||||
class ContentTooLargeException extends \Exception
|
||||
{
|
||||
/**
|
||||
* The URI of the file that exceeded the limit
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $uri;
|
||||
|
||||
/**
|
||||
* The size of the file in bytes
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $size;
|
||||
|
||||
/**
|
||||
* The limit that was exceeded in bytes
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $limit;
|
||||
|
||||
/**
|
||||
* @param string $uri The URI of the file that exceeded the limit
|
||||
* @param int $size The size of the file in bytes
|
||||
* @param int $limit The limit that was exceeded in bytes
|
||||
* @param \Throwable $previous The previous exception used for the exception chaining.
|
||||
*/
|
||||
public function __construct(string $uri, int $size, int $limit, \Throwable $previous = null)
|
||||
{
|
||||
$this->uri = $uri;
|
||||
$this->size = $size;
|
||||
$this->limit = $limit;
|
||||
parent::__construct("$uri exceeds size limit of $limit bytes ($size)", 0, $previous);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer;
|
||||
|
||||
use LanguageServer\Index\ReadableIndex;
|
||||
use phpDocumentor\Reflection\{Types, Type, TypeResolver};
|
||||
use LanguageServerProtocol\SymbolInformation;
|
||||
use Generator;
|
||||
|
||||
/**
|
||||
* Class used to represent symbols
|
||||
*/
|
||||
class Definition
|
||||
{
|
||||
/**
|
||||
* The fully qualified name of the symbol, if it has one
|
||||
*
|
||||
* Examples of FQNs:
|
||||
* - testFunction()
|
||||
* - TestNamespace
|
||||
* - TestNamespace\TestClass
|
||||
* - TestNamespace\TestClass::TEST_CONSTANT
|
||||
* - TestNamespace\TestClass::$staticTestProperty
|
||||
* - TestNamespace\TestClass->testProperty
|
||||
* - TestNamespace\TestClass::staticTestMethod()
|
||||
* - TestNamespace\TestClass->testMethod()
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $fqn;
|
||||
|
||||
/**
|
||||
* For class or interfaces, the FQNs of extended classes and implemented interfaces
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
public $extends;
|
||||
|
||||
/**
|
||||
* 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 $isMember;
|
||||
|
||||
/**
|
||||
* True if this definition is affected by global namespace fallback (global function or global constant)
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $roamed;
|
||||
|
||||
/**
|
||||
* False for instance methods and properties
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $isStatic;
|
||||
|
||||
/**
|
||||
* True if the Definition is a class
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $canBeInstantiated;
|
||||
|
||||
/**
|
||||
* @var SymbolInformation
|
||||
*/
|
||||
public $symbolInformation;
|
||||
|
||||
/**
|
||||
* The type a reference to this symbol will resolve to.
|
||||
* For properties and constants, this is the type of the property/constant.
|
||||
* 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_.
|
||||
*
|
||||
* @var Type|null
|
||||
*/
|
||||
public $type;
|
||||
|
||||
/**
|
||||
* The first line of the declaration, for use in textDocument/hover
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $declarationLine;
|
||||
|
||||
/**
|
||||
* A documentation string, for use in textDocument/hover
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\Factory;
|
||||
|
||||
use LanguageServerProtocol\Location;
|
||||
use LanguageServerProtocol\SymbolInformation;
|
||||
use LanguageServerProtocol\SymbolKind;
|
||||
use Microsoft\PhpParser\Node;
|
||||
use Microsoft\PhpParser\ResolvedName;
|
||||
use LanguageServer\Factory\LocationFactory;
|
||||
|
||||
class SymbolInformationFactory
|
||||
{
|
||||
/**
|
||||
* Converts a Node to a SymbolInformation
|
||||
*
|
||||
* @param Node $node
|
||||
* @param string $fqn If given, $containerName will be extracted from it
|
||||
* @return SymbolInformation|null
|
||||
*/
|
||||
public static function fromNode($node, string $fqn = null)
|
||||
{
|
||||
$symbol = new SymbolInformation();
|
||||
if ($node instanceof Node\Statement\ClassDeclaration) {
|
||||
$symbol->kind = SymbolKind::CLASS_;
|
||||
} else if ($node instanceof Node\Statement\TraitDeclaration) {
|
||||
$symbol->kind = SymbolKind::CLASS_;
|
||||
} else if (\LanguageServer\ParserHelpers\isConstDefineExpression($node)) {
|
||||
// constants with define() like
|
||||
// define('TEST_DEFINE_CONSTANT', false);
|
||||
$symbol->kind = SymbolKind::CONSTANT;
|
||||
$symbol->name = $node->argumentExpressionList->children[0]->expression->getStringContentsText();
|
||||
} else if ($node instanceof Node\Statement\InterfaceDeclaration) {
|
||||
$symbol->kind = SymbolKind::INTERFACE;
|
||||
} else if ($node instanceof Node\Statement\NamespaceDefinition) {
|
||||
$symbol->kind = SymbolKind::NAMESPACE;
|
||||
} else if ($node instanceof Node\Statement\FunctionDeclaration) {
|
||||
$symbol->kind = SymbolKind::FUNCTION;
|
||||
} else if ($node instanceof Node\MethodDeclaration) {
|
||||
$nameText = $node->getName();
|
||||
if ($nameText === '__construct' || $nameText === '__destruct') {
|
||||
$symbol->kind = SymbolKind::CONSTRUCTOR;
|
||||
} else {
|
||||
$symbol->kind = SymbolKind::METHOD;
|
||||
}
|
||||
} else if ($node instanceof Node\Expression\Variable && $node->getFirstAncestor(Node\PropertyDeclaration::class) !== null) {
|
||||
$symbol->kind = SymbolKind::PROPERTY;
|
||||
} else if ($node instanceof Node\ConstElement) {
|
||||
$symbol->kind = SymbolKind::CONSTANT;
|
||||
} else if (
|
||||
(
|
||||
($node instanceof Node\Expression\AssignmentExpression)
|
||||
&& $node->leftOperand instanceof Node\Expression\Variable
|
||||
)
|
||||
|| $node instanceof Node\UseVariableName
|
||||
|| $node instanceof Node\Parameter
|
||||
) {
|
||||
$symbol->kind = SymbolKind::VARIABLE;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($node instanceof Node\Expression\AssignmentExpression) {
|
||||
if ($node->leftOperand instanceof Node\Expression\Variable) {
|
||||
$symbol->name = $node->leftOperand->getName();
|
||||
} elseif ($node->leftOperand instanceof PhpParser\Token) {
|
||||
$symbol->name = trim($node->leftOperand->getText($node->getFileContents()), "$");
|
||||
}
|
||||
} else if ($node instanceof Node\UseVariableName) {
|
||||
$symbol->name = $node->getName();
|
||||
} else if (isset($node->name)) {
|
||||
if ($node->name instanceof Node\QualifiedName) {
|
||||
$symbol->name = (string)ResolvedName::buildName($node->name->nameParts, $node->getFileContents());
|
||||
} else {
|
||||
$symbol->name = ltrim((string)$node->name->getText($node->getFileContents()), "$");
|
||||
}
|
||||
} else if (isset($node->variableName)) {
|
||||
$symbol->name = $node->variableName->getText($node);
|
||||
} else if (!isset($symbol->name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$symbol->location = LocationFactory::fromNode($node);
|
||||
if ($fqn !== null) {
|
||||
$parts = preg_split('/(::|->|\\\\)/', $fqn);
|
||||
array_pop($parts);
|
||||
$symbol->containerName = implode('\\', $parts);
|
||||
}
|
||||
return $symbol;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\FilesFinder;
|
||||
|
||||
use LanguageServer\LanguageClient;
|
||||
use Sabre\Event\Promise;
|
||||
use Sabre\Uri;
|
||||
use Webmozart\Glob\Glob;
|
||||
|
||||
/**
|
||||
* Retrieves file content from the client through a textDocument/xcontent request
|
||||
*/
|
||||
class ClientFilesFinder implements FilesFinder
|
||||
{
|
||||
/**
|
||||
* @var LanguageClient
|
||||
*/
|
||||
private $client;
|
||||
|
||||
/**
|
||||
* @param LanguageClient $client
|
||||
*/
|
||||
public function __construct(LanguageClient $client)
|
||||
{
|
||||
$this->client = $client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all files in the workspace that match a glob.
|
||||
* If the client does not support workspace/files, it falls back to searching the file system directly.
|
||||
*
|
||||
* @param string $glob
|
||||
* @return Promise <string[]> The URIs
|
||||
*/
|
||||
public function find(string $glob): Promise
|
||||
{
|
||||
return $this->client->workspace->xfiles()->then(function (array $textDocuments) use ($glob) {
|
||||
$uris = [];
|
||||
foreach ($textDocuments as $textDocument) {
|
||||
$path = Uri\parse($textDocument->uri)['path'];
|
||||
if (Glob::match($path, $glob)) {
|
||||
$uris[] = $textDocument->uri;
|
||||
}
|
||||
}
|
||||
return $uris;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\FilesFinder;
|
||||
|
||||
use Sabre\Event\Promise;
|
||||
use function Sabre\Event\coroutine;
|
||||
use function LanguageServer\{pathToUri, timeout};
|
||||
|
||||
class FileSystemFilesFinder implements FilesFinder
|
||||
{
|
||||
/**
|
||||
* Returns all files in the workspace that match a glob.
|
||||
* If the client does not support workspace/xfiles, it falls back to searching the file system directly.
|
||||
*
|
||||
* @param string $glob
|
||||
* @return Promise <string[]>
|
||||
*/
|
||||
public function find(string $glob): Promise
|
||||
{
|
||||
return coroutine(function () use ($glob) {
|
||||
$uris = [];
|
||||
foreach (new GlobIterator($glob) as $path) {
|
||||
// Exclude any directories that also match the glob pattern
|
||||
if (!is_dir($path) || !is_readable($path)) {
|
||||
$uris[] = pathToUri($path);
|
||||
}
|
||||
|
||||
yield timeout();
|
||||
}
|
||||
return $uris;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\FilesFinder;
|
||||
|
||||
use Sabre\Event\Promise;
|
||||
|
||||
/**
|
||||
* Interface for finding files in the workspace
|
||||
*/
|
||||
interface FilesFinder
|
||||
{
|
||||
/**
|
||||
* Returns all files in the workspace that match a glob.
|
||||
* If the client does not support workspace/xfiles, it falls back to searching the file system directly.
|
||||
*
|
||||
* @param string $glob
|
||||
* @return Promise <string[]>
|
||||
*/
|
||||
public function find(string $glob): Promise;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
|
||||
namespace LanguageServer\FqnUtilities;
|
||||
|
||||
use phpDocumentor\Reflection\{Type, Types};
|
||||
|
||||
/**
|
||||
* Returns all possible FQNs in a type
|
||||
*
|
||||
* @param Type|null $type
|
||||
* @return string[]
|
||||
*/
|
||||
function getFqnsFromType($type): array
|
||||
{
|
||||
$fqns = [];
|
||||
if ($type instanceof Types\Object_) {
|
||||
$fqsen = $type->getFqsen();
|
||||
if ($fqsen !== null) {
|
||||
$fqns[] = substr((string)$fqsen, 1);
|
||||
}
|
||||
}
|
||||
if ($type instanceof Types\Compound) {
|
||||
for ($i = 0; $t = $type->get($i); $i++) {
|
||||
foreach (getFqnsFromType($t) as $fqn) {
|
||||
$fqns[] = $fqn;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $fqns;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer;
|
||||
|
||||
/**
|
||||
* Generates unique, incremental IDs for use as request IDs
|
||||
*/
|
||||
class IdGenerator
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $counter = 1;
|
||||
|
||||
/**
|
||||
* Returns a unique ID
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function generate()
|
||||
{
|
||||
return $this->counter++;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\Index;
|
||||
|
||||
use LanguageServer\Definition;
|
||||
use Sabre\Event\EmitterTrait;
|
||||
|
||||
abstract class AbstractAggregateIndex implements ReadableIndex
|
||||
{
|
||||
use EmitterTrait;
|
||||
|
||||
/**
|
||||
* Returns all indexes managed by the aggregate index
|
||||
*
|
||||
* @return ReadableIndex[]
|
||||
*/
|
||||
abstract protected function getIndexes(): array;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
foreach ($this->getIndexes() as $index) {
|
||||
$this->registerIndex($index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ReadableIndex $index
|
||||
*/
|
||||
protected function registerIndex(ReadableIndex $index)
|
||||
{
|
||||
$index->on('complete', function () {
|
||||
if ($this->isComplete()) {
|
||||
$this->emit('complete');
|
||||
}
|
||||
});
|
||||
$index->on('static-complete', function () {
|
||||
if ($this->isStaticComplete()) {
|
||||
$this->emit('static-complete');
|
||||
}
|
||||
});
|
||||
$index->on('definition-added', function () {
|
||||
$this->emit('definition-added');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks this index as complete
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setComplete()
|
||||
{
|
||||
foreach ($this->getIndexes() as $index) {
|
||||
$index->setComplete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks this index as complete for static definitions and references
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setStaticComplete()
|
||||
{
|
||||
foreach ($this->getIndexes() as $index) {
|
||||
$index->setStaticComplete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this index is complete
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isComplete(): bool
|
||||
{
|
||||
foreach ($this->getIndexes() as $index) {
|
||||
if (!$index->isComplete()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this index is complete for static definitions or references
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isStaticComplete(): bool
|
||||
{
|
||||
foreach ($this->getIndexes() as $index) {
|
||||
if (!$index->isStaticComplete()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Generator providing an associative array [string => Definition]
|
||||
* that maps fully qualified symbol names to Definitions (global or not)
|
||||
*
|
||||
* @return \Generator yields Definition
|
||||
*/
|
||||
public function getDefinitions(): \Generator
|
||||
{
|
||||
foreach ($this->getIndexes() as $index) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Definition object by a specific FQN
|
||||
*
|
||||
* @param string $fqn
|
||||
* @param bool $globalFallback Whether to fallback to global if the namespaced FQN was not found
|
||||
* @return Definition|null
|
||||
*/
|
||||
public function getDefinition(string $fqn, bool $globalFallback = false)
|
||||
{
|
||||
foreach ($this->getIndexes() as $index) {
|
||||
if ($def = $index->getDefinition($fqn, $globalFallback)) {
|
||||
return $def;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Generator providing all URIs in this index that reference a symbol
|
||||
*
|
||||
* @param string $fqn The fully qualified name of the symbol
|
||||
* @return \Generator yields string
|
||||
*/
|
||||
public function getReferenceUris(string $fqn): \Generator
|
||||
{
|
||||
foreach ($this->getIndexes() as $index) {
|
||||
yield from $index->getReferenceUris($fqn);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace LanguageServer\Index;
|
||||
|
||||
class DependenciesIndex extends AbstractAggregateIndex
|
||||
{
|
||||
/**
|
||||
* Map from package name to index
|
||||
*
|
||||
* @var Index[]
|
||||
*/
|
||||
protected $indexes = [];
|
||||
|
||||
/**
|
||||
* @return Index[]
|
||||
*/
|
||||
protected function getIndexes(): array
|
||||
{
|
||||
return $this->indexes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return Index
|
||||
*/
|
||||
public function getDependencyIndex(string $packageName): Index
|
||||
{
|
||||
if (!isset($this->indexes[$packageName])) {
|
||||
$index = new Index;
|
||||
$this->indexes[$packageName] = $index;
|
||||
$this->registerIndex($index);
|
||||
}
|
||||
return $this->indexes[$packageName];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @param Index $index
|
||||
* @return void
|
||||
*/
|
||||
public function setDependencyIndex(string $packageName, Index $index)
|
||||
{
|
||||
$this->indexes[$packageName] = $index;
|
||||
$this->registerIndex($index);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return void
|
||||
*/
|
||||
public function removeDependencyIndex(string $packageName)
|
||||
{
|
||||
unset($this->indexes[$packageName]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return bool
|
||||
*/
|
||||
public function hasDependencyIndex(string $packageName): bool
|
||||
{
|
||||
return isset($this->indexes[$packageName]);
|
||||
}
|
||||
}
|