1
0
Fork 0

Compare commits

...

129 Commits

Author SHA1 Message Date
Carl Kittelberger fb48c70ce2
Rename package and mark it as replacing original package. 2020-01-13 17:05:15 +01:00
Carl Kittelberger 3fc105717d
Implement existing fix from https://github.com/felixfbecker/php-language-server/issues/462#issuecomment-546696984. 2020-01-13 16:52:36 +01:00
Felix Becker 9dc1656592
build: remove composer install from semantic-release config 2018-12-12 16:29:42 +01:00
Jakob Blume 7303143a60 build: run 'composer install' in a docker builder stage (#694) 2018-12-12 16:28:19 +01:00
Tyson Andre 1705583e32 chore: add Phan (#690) 2018-11-29 09:50:01 +01:00
Felix Becker 1da3328bc2 fix: allow rootUri to be null
Fixes #684
2018-11-13 18:33:21 +01:00
Tyson Andre 450116e2f3 docs: remove unused use statements, nit on phpdoc (#625)
* Remove unused use statements, nit on phpdoc

Add a note on something that looks like an invalid array index

* Remove phpdoc param with no real param
2018-11-11 20:45:47 +01:00
Felix Becker b1cc565d7e fix(cache): bump cache version 2018-11-11 12:57:20 +01:00
Matthew Brown ed2d8ddb1e refactor: fix impossible parse_url equality (#676)
`parse_url` returns `false` for malformed urls, not `null`
2018-11-11 04:47:10 +01:00
JJK96 680f430453 fix: support rootUri (#672) 2018-11-11 04:33:12 +01:00
Dylan McGannon c7d25c7b44 fix(definitionresolver): infinite loop when indexing self referencing classes (#670) 2018-11-11 04:26:39 +01:00
Felix Becker 71390c9903 chore: update package-lock.json 2018-11-11 04:07:16 +01:00
Michael V 24388bcf26 perf: change index to a tree to speed up completion (#680)
Refactors Index into a tree structure, rather than an array of Fqns to definitions.

Closes #274
2018-11-11 03:47:57 +01:00
dantleech 18c6ccd137 refactor: use protocol package (#661)
Adapts the Language Server to use the extracted php language server protocol
2018-09-09 14:37:35 +02:00
Felix Becker 3d8318bd03
docs: bye Gemnasium 2018-09-06 17:10:39 +02:00
Felix Becker b4b4a2fff5 chore: update semantic-release dependencies 2018-08-22 21:25:11 +02:00
janekcz69 3931c8848f fix: cast null to array before passing to array_merge() (#666)
Fixes #595
2018-08-22 20:48:14 +02:00
Markus Staab 26e3451e61 docs: add a TOC to the README (#618) 2018-05-13 17:26:56 -07:00
Felix Becker fe33c8cd7f fix(package): include tests folder
This is needed if you want to extend the buildserver and reuse tests
2018-04-26 15:53:21 -07:00
Felix Becker 7e1ca75863 ci(travis): remove brew tap 2018-04-21 16:52:37 -07:00
Felix Becker ebf4c096b3 ci(travis): use PECL to install XDebug 2018-04-06 13:00:32 -07:00
Vincent Klaiber 49f1e8f04a chore: exclude appveyor.yml in production 2018-03-14 10:33:50 -07:00
Declspeck 02b7d2fdb6 feat(completion): add pseudo-keywords like int, bool, strict_types to completion 2018-03-11 13:50:12 -07:00
Felix Becker de1af6a165
refactor: use composer/xdebug-handler (#616) 2018-03-08 11:48:56 -08:00
Declspeck e10896f905 test(performance): don't eat exceptions during benchmark 2018-02-28 11:21:04 -08:00
Declspeck b412c125a4 fix(indexing): handle integer FQNs 2018-02-28 11:21:04 -08:00
Tyson Andre 8adcf92c2f chore: remove unused 'use' statements (#612)
detected via static analysis and manually checked
2018-02-28 10:05:22 -08:00
Felix Becker fc6b069425 ci(dependencies.io): track stable semantic-release 2018-02-27 21:05:16 -08:00
Vincent Klaiber c5a83af327 ci(travis): update travis php versions (#601) 2018-02-27 21:03:30 -08:00
Jens Hausdorf a8f60c9cf6 fix(completion): do not propose <?php if completion context is not given (#593)
fixes #372
2018-02-07 11:55:25 -08:00
Felix Becker d9bc0b0285
fix(completion): don't require constructor parameter for protocol DTO (#592) 2018-02-02 12:09:25 -08:00
Phil Nelson 6894d85aaf fix(DefinitionResolver): resolve self correctly for docblock @return self (#576) 2018-01-09 01:38:18 -08:00
Tyson Andre c48ee55808 tests: fix benchmark on case sensitive filesystems (#573)
On case insensitive file systems, such as the defaults for Mac OS/Windows, this works, but it doesn't work for ext4, etc.

The folder being checked out is `validation/frameworks/codeigniter`, this searched for `validation/frameworks/CodeIgniter`
2018-01-01 18:31:55 -08:00
Felix Becker 20960a8b9f
fix(DefinitionResolver): find variables in sibling children (#568)
Fixes #566
2017-12-30 22:26:51 -08:00
Felix Becker 8439da999a ci(travis): only build master and PRs 2017-12-28 15:34:11 -08:00
Phil Nelson 1cfba8b6bb fix(DefinitionResolver): don't crash if foreach key isn't a variable (#564) 2017-12-24 17:55:48 -08:00
Phil Nelson 425b2390b5 fix(DefinitionResolver): fix crash on unknown foreach type (#562)
Fix when unknown type is found in foreach expression
2017-12-24 01:52:49 -08:00
Jannik Vieten a0caf8d18f docs(used-by): mention Atom's ide-php in README (#559)
adds Atom's ide-php package to "used by" section in README
2017-12-22 18:03:24 -08:00
Phil Nelson 63da051e72 fix(DefinitionResolver): fix methods with self return type (#550) 2017-12-22 18:02:37 -08:00
Phil Nelson 9eea26df71 feat: foreach completion (#551) 2017-12-17 17:55:12 -08:00
Felix Becker f46fccd0d3 docs: add missing completion gif 2017-12-09 21:44:03 -08:00
Felix Becker 6d0a7ba7df docs: add signatureHelp demo 2017-12-09 21:41:36 -08:00
phil-nelson a40cf731f7 feat: Signature help (#547)
closes #18
2017-12-09 21:10:43 -08:00
Felix Becker 78316545a8 ci(macos): try alternative method to download composer 2017-12-03 16:23:14 -08:00
Felix Becker 09477b747e fix(diagnostics): handle null case 2017-12-03 15:49:43 -08:00
Maarten Staa 9b1fafae58 fix(diagnostics): update checking of $this usage to only error in static methods (#545) 2017-12-03 13:42:01 -08:00
Felix Becker ff746a836d chore: update semantic-release to v11 2017-11-25 10:52:21 -08:00
Felix Becker 31bae23912 ci(release): use semantic-release v10 2017-11-22 03:33:35 -08:00
Felix Becker 724eb6f1dc ci(appveyor): update image 2017-11-21 03:41:03 -08:00
Maarten Staa 4f672c24d8 feat(diagnostics): report error when $this is used in a static method or outside a class method (#528) 2017-11-18 17:41:37 -08:00
Felix Becker 80ef8ff503
fix(indexing): properly resolve self, static and parent keywords (#532)
Previously we would dump static, self and parent as literal FQNs into the index.
2017-11-18 16:59:57 -08:00
Felix Becker b1a1875070
fix(completion): don't suggest <?php on > characer (#527)
closes #372
2017-11-15 22:38:01 -08:00
Felix Becker 06747bb734 ci(travis): don't release on PRs 2017-11-15 13:14:08 -08:00
Felix Becker 607cd8158d test(index): add IndexTest 2017-11-15 13:08:15 -08:00
Felix Becker 1ec8d8d8e2
ci(travis): correct version 2017-11-12 12:41:51 -08:00
Felix Becker 0afc3320d5
ci(travis): pin version to 7.2RC5
7.2RC6 is causing segfaults
2017-11-12 12:40:13 -08:00
Felix Becker 1804ac8d97 ci(travis): correct BUILD_LEADER_ID 2017-11-10 01:16:15 -08:00
Felix Becker 9434cb1b67 ci(release): set verifyConditions to empty array 2017-11-10 00:26:03 -08:00
Felix Becker 0e645301cc ci(travis): remove language tag 2017-11-09 22:40:56 -08:00
Felix Becker 3e41244b6f ci(travis): use PHP 7 for release 2017-11-09 22:01:50 -08:00
Felix Becker eadf305a1f ci(travis): fix release 2017-11-09 19:07:43 -08:00
Felix Becker d54ece3366 build(docker): optimize docker build 2017-11-09 18:59:41 -08:00
Felix Becker 857fe26eb5 ci(travis): optimize 2017-11-09 18:48:02 -08:00
Felix Becker b4a3134e2a ci(travis): use build stages 2017-11-09 18:00:39 -08:00
Brandon Max f5c45f83ed docs(contributing): document how to use XDebug (#518) 2017-11-09 16:15:36 -08:00
Felix Becker b03b9a239c
ci(travis): run on OSX (#517) 2017-11-05 02:54:56 -08:00
Felix Becker 41e84880b3 ci(travis): use string versions 2017-11-05 02:30:34 -08:00
Felix Becker 74578c7b58 ci(travis): only test lowest and highest PHP version 2017-11-05 02:28:38 -08:00
Felix Becker 235a790156 ci: remove shallow submodule cloning 2017-11-05 01:51:33 -08:00
Felix Becker db484617b6 ci: speed up submodule cloning 2017-11-05 01:44:48 -08:00
Felix Becker f00fd1b62c
fix(formatting): drop PHP CodeSniffer (#504)
At this point there are countless issues about the formatting done by CodeSniffer. It plain out doesn't work in many cases, overrides format options that are contributed by other extensions in VS Code and does not reuse any of our AST parsing. For that reason, I am starting to think there is no reason to keep it in here until we have proper pretty-printing support from https://github.com/Microsoft/tolerant-php-parser that actually reuses our ASTs and can work while editing. For people who want to use CodeSniffer to format their code, there could be a standalone CodeSniffer language server (like there is a TSLint language server and ESLint language server). As said, we don't reuse our state anyway.

BREAKING CHANGE: removes formatting support

closes #501
closes #474
closes #473
closes #468
closes #450
closes #445
closes #443
closes #423
closes #343
closes #296
closes #293
closes #499
closes #471
2017-11-04 23:57:51 -07:00
Felix Becker e9fc97d430 chore: extend export-ignore file list 2017-11-01 23:39:38 -07:00
Nate Eagleson 6dbeef63bc docs: correct parse-stubs section in readme (#502)
As the parse-stubs step is done automatically by `composer install` since 34d3d2030d, we no longer need to explicitly instruct people to do it.
Note that sometimes you must parse the PHP stubs manually
2017-11-01 09:38:54 -07:00
Felix Becker ac6bce929f chore: get patch versions of tolerant-php-parser 2017-10-30 22:51:23 -07:00
Felix Becker d3c9133892 ci(appveyor): cache chocolatey downloads 2017-10-30 21:12:44 -07:00
Jens Hausdorf 1edbe35609 refactor: use FunctionLike Interface (#505) 2017-10-30 03:33:19 -07:00
Felix Becker 744062c14e ci: add AppVeyor to test Windows
closes #40
2017-10-30 03:09:06 -07:00
Felix Becker 7ae6452d1a
refactor(index): rename isGlobal to isMember (#511)
isGlobal was confusing because a non-member can be considered global vs namespaced
2017-10-29 17:45:06 -07:00
Felix Becker c74076d84f
fix(cache): bump cache version (#508)
the update of reflection-docblock means old caches are no longer valid.

fixes #507
2017-10-29 13:06:44 -07:00
Felix Becker 99d8a361db build: fix typo in release-docker script 2017-10-28 14:24:36 -07:00
Felix Becker 9e551a310b build: use PHP for release-docker script 2017-10-28 13:59:02 -07:00
Felix Becker b86d6c96c7 build: make release-docker.sh executable 2017-10-28 13:38:17 -07:00
Felix Becker 95f49d3a70 ci: set BUILD_LEADER_ID
see https://github.com/semantic-release/travis-deploy-once/issues/22
2017-10-28 13:18:41 -07:00
Jens Hausdorf fbaa7b3cc5 refactor: use ClassLike interface (#506) 2017-10-28 12:27:32 -07:00
Tyson Andre 1db6b7bbb3 chore: fixes for unused variables and phpdoc (#496)
The identifier doesn't need to be generated for a notification to the
client, since there's no response
Add undeclared properties to TreeAnalyzer
Fix other bugs in phpdoc
2017-10-22 22:54:38 -07:00
Felix Becker 16cf8f53e9 fix(docblocks): update to phpdocumentor/reflection-docblock ^4.0.0
closes #139
2017-10-22 21:30:38 -07:00
Felix Becker 4384d49414 ci(travis): remove redundant parse-stubs step 2017-10-22 17:22:28 -07:00
Felix Becker a934aff7a9 ci(release): use semantic-release 2017-10-22 17:22:02 -07:00
Dependencies.io Bot 7b1176dd9d ci(dependencies.io): add dependencies.yml config 2017-10-22 17:22:02 -07:00
Felix Becker 1240f25e01 Update parser 2017-10-19 14:45:36 -07:00
Felix Becker 19bf94ac7b Improve README 2017-10-19 14:44:56 -07:00
Felix Becker e31f7b5923 Add more Composer scripts 2017-10-19 14:38:20 -07:00
Vincent Klaiber 0c399150a3 Update travis and phpunit (#489) 2017-10-02 14:11:06 -07:00
Vincent Klaiber b9ebfb52c9 Update composer.json structure (#487) 2017-10-02 13:58:37 -07:00
Vincent Klaiber 3d8655d504 Update phpunit config (#488)
* Update phpunit config

* Rename DocumentHighlight class
2017-10-02 13:37:28 -07:00
Vincent Klaiber d24c42008e Exclude non-essential files in .gitattributes (#486)
* Exclude non-essential files in .gitattributes

https://www.reddit.com/r/PHP/comments/2jzp6k/i_dont_need_your_tests_in_my_production/

* Add validation and .gitmodules
2017-10-02 13:36:04 -07:00
Stephan Unverwerth d4443465bb Fix missing diagnostics for nodes (#484)
* Fix missing diagnostics for nodes

* Refactor TreeAnalyzer
2017-09-28 12:53:12 -07:00
John Nguyen a4739430f8 Fix memory leak issue (#459)
Closes #425
2017-08-21 22:43:17 -07:00
Rob Lourens 63bf43e40c Bump tolerant-php-parser to get fix (#457)
for https://github.com/Microsoft/tolerant-php-parser/issues/12
2017-08-11 10:29:55 -07:00
Felix Becker 7ce2284176 Pin phpdocumentor/reflection-docblock dependency
https://github.com/phpDocumentor/ReflectionDocBlock/issues/109
2017-07-19 13:15:48 +02:00
Ivan Bozhanov 35f33c8c91 Fluent interfaces support (#421) 2017-07-07 13:18:19 +02:00
Felix Becker 94fc0405fd Correct parser link in README 2017-07-01 14:32:56 +02:00
Felix Becker fc0bf4c163 Fix workspace/xreferences (#424)
* Make Descriptors minimal

SymbolDescriptor and PackageDescriptor should only contain the minumum amount of properties needed

* Add missing use

* Fixes

* Ignore ReferenceInformation->symbol
2017-06-22 20:06:10 +02:00
Felix Becker fced1d5af6 Fix textDocument/xdefinition (#429) 2017-06-22 17:34:28 +02:00
Felix Becker 00552120ad Restrict workspace/symbol results to non-dependency symbols (#426)
This improves performance a lot and matches what other language servers do
2017-06-21 14:17:36 +02:00
Felix Becker f43ce50d5a Default memory limit to 4GB 2017-06-21 11:48:41 +02:00
Felix Becker 08fe84de35 Add launch.json 2017-06-20 08:38:06 +02:00
Rob Lourens a454cd2873 Add vendor/validation folders to search.exclude (#420) 2017-06-20 08:35:47 +02:00
Ivan Bozhanov dae3f2576c Add $this completion (#419) 2017-06-19 12:23:43 +02:00
Rob Lourens f97105740d Bump tolerant-php-parser (#415)
* Bump tolerant-php-parser

* Update test for new parser static support
2017-06-17 10:53:08 +02:00
Felix Becker 548120314d Revert "Update CodeSniffer"
This reverts commit 663ccd5f23.
2017-06-16 20:39:32 +02:00
Felix Becker a772d9a2d7 Remove content (#413) 2017-06-16 20:31:29 +02:00
Felix Becker 0e3727a8d6 Improve CompletionProvider (#412)
- Better performance
- More documentation
- Add field to Definition for global namespace fallback

Fixes #380
2017-06-16 20:31:13 +02:00
Felix Becker 663ccd5f23 Update CodeSniffer 2017-06-15 17:11:57 +02:00
Felix Becker 4a98afe540 Fix docblock union types 2017-06-15 17:03:25 +02:00
Rob Lourens 3b633369a7 Fix error getting completions for 'new static' type (#405) 2017-06-15 12:44:03 +02:00
Nicholas Narsing 8d1732ed02 Exclude directory paths from file system search (#401)
* Exclude directories from file system search

Directories can also match the glob search pattern if their names end in ".php", which will cause a read error later since the ContentRetriever implementers are expecting files. As far as I know, the only way to fix this is to do an additional check to ensure the URI is not of a directory.

This resolves #306.
2017-06-11 23:24:17 +02:00
Felix Becker fe7e9d5800 Rename $stmts to $sourceFileNode everywhere
The root node is now a SourceFileNode, not an array
2017-06-10 21:36:16 +02:00
Jens Hausdorf 4c1d7bd1bc Add true, false, null to keywords (#396) 2017-06-10 18:47:19 +02:00
Stephan Unverwerth cc3f0da21a Fix 'find references' for unused symbols (#392)
* Add tests for unused symbols

* Fix tests for unused symbols
2017-06-10 11:37:39 +02:00
Rob Lourens f10680e441 Fix variable type from method return value, add tests (#393) 2017-06-10 11:10:15 +02:00
Rob Lourens 7b72b38fd9 Assert that references array is equal, not a subset, and update expected.json files (#395) 2017-06-10 10:55:41 +02:00
Jens Hausdorf 42d0c7b714 Improve handling of abstract classes (#391) 2017-06-09 22:12:32 +02:00
Sara Itani 7f427a1215 Adopt Microsoft/tolerant-php-parser (#357) 2017-06-09 20:25:30 +02:00
Stephan Unverwerth 08cf1a3fd7 Allow getting type from define() node (#363)
* Allow getting type from define() node
- fixes #364

* Add test case for DefinitionResolver
2017-04-24 11:11:40 +02:00
Jens Hausdorf b1cc7bf6b0 Support constants with define() (#347) 2017-04-17 17:03:08 +02:00
Jens Hausdorf de6aed608c Show constructors and destructors with right symbol (#346) 2017-04-09 19:44:28 +02:00
Jens Hausdorf 97d1579f37 Update PHPParser dependency (#345) 2017-04-09 18:23:46 +02:00
Sara Itani f50df5cdaf Enforce memory limit in phpunit.xml (#320)
This will help highlight memory regressions, make it easier for newcomers to get started with the codebase w/o editing php.ini defaults (128M), and also keep things consistent between local and travis runs.
2017-04-02 14:08:45 +02:00
315 changed files with 17638 additions and 3802 deletions

View File

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

View File

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

24
.gitattributes vendored Normal file
View File

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

3
.gitignore vendored
View File

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

27
.gitmodules vendored Normal file
View File

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

308
.phan/config.php Normal file
View File

@ -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',
],
];

View File

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

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

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

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

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

View File

@ -1,22 +1,19 @@
# Running this container will start a language server that listens for TCP connections on port 2088
# Every connection will be run in a forked child process
# Please note that before building the image, you have to install dependencies with `composer install`
FROM composer AS builder
COPY ./ /app
RUN composer install
FROM php:7-cli
MAINTAINER Felix Becker <felix.b@outlook.com>
RUN apt-get update \
# Needed for CodeSniffer
&& apt-get install -y libxml2 libxml2-dev \
&& rm -rf /var/lib/apt/lists/*
LABEL maintainer="Felix Becker <felix.b@outlook.com>"
RUN docker-php-ext-configure pcntl --enable-pcntl
RUN docker-php-ext-install pcntl
COPY ./php.ini /usr/local/etc/php/conf.d/
COPY ./ /srv/phpls
COPY --from=builder /app /srv/phpls
WORKDIR /srv/phpls

View File

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

54
appveyor.yml Normal file
View File

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

89
benchmarks/completion.php Normal file
View File

@ -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;

69
benchmarks/parsing.php Normal file
View File

@ -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;
}

View File

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

View File

@ -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,33 +14,43 @@
"autocompletion",
"refactor"
],
"bin": ["bin/php-language-server.php"],
"scripts": {
"parse-stubs": "LanguageServer\\ComposerScripts::parseStubs",
"post-install-cmd": "@parse-stubs"
},
"authors": [
{
"name": "Felix Becker",
"email": "felix.b@outlook.com"
}
],
"require": {
"php": ">=7.0",
"nikic/php-parser": "^3.0.4",
"phpdocumentor/reflection-docblock": "^3.0",
"sabre/event": "^5.0",
"felixfbecker/advanced-json-rpc": "^2.0",
"squizlabs/php_codesniffer" : "3.0.0RC3",
"netresearch/jsonmapper": "^1.0",
"webmozart/path-util": "^2.3",
"webmozart/glob": "^4.1",
"sabre/uri": "^2.0",
"php": "^7.0",
"composer/xdebug-handler": "^1.0",
"felixfbecker/advanced-json-rpc": "^3.0.0",
"felixfbecker/language-server-protocol": "^1.0.1",
"jetbrains/phpstorm-stubs": "dev-master",
"composer/composer": "^1.3"
"microsoft/tolerant-php-parser": "0.0.*",
"netresearch/jsonmapper": "^1.0",
"phpdocumentor/reflection-docblock": "^4.0.0",
"psr/log": "^1.0",
"sabre/event": "^5.0",
"sabre/uri": "^2.0",
"webmozart/glob": "^4.1",
"webmozart/path-util": "^2.3"
},
"require-dev": {
"phpunit/phpunit": "^6.3",
"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/utils.php",
"src/FqnUtilities.php",
"src/ParserHelpers.php"
]
},
"autoload-dev": {
@ -55,8 +58,19 @@
"LanguageServer\\Tests\\": "tests/"
}
},
"require-dev": {
"phpunit/phpunit": "^5.5",
"phpunit/php-code-coverage": "^4.0"
}
"bin": [
"bin/php-language-server.php"
],
"scripts": {
"parse-stubs": "LanguageServer\\ComposerScripts::parseStubs",
"post-install-cmd": "@parse-stubs",
"post-update-cmd": "@parse-stubs",
"test": "vendor/bin/phpunit",
"lint": "vendor/bin/phpcs"
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true
}

17
dependencies.yml Normal file
View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
<

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,10 @@
<?php
namespace Whatever;
use TestNamespace\InnerNamespace as AliasNamespace;
class IDontShowUpInCompletion {}
AliasNamespace\I;
AliasNamespace\;

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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;

View File

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

View File

@ -98,3 +98,13 @@ new class {
};
class ChildClass extends TestClass {}
class Example {
public function __construct() {}
public function __destruct() {}
}
namespace TestNamespace\InnerNamespace;
class InnerClass {
}

BIN
images/completion.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

BIN
images/signatureHelp.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

6660
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

31
package.json Normal file
View File

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

View File

@ -2,9 +2,12 @@
<ruleset name="PHP Language Server">
<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>

View File

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

View File

@ -11,6 +11,11 @@ use Sabre\Event\Promise;
*/
class ClientCache implements Cache
{
/**
* @var LanguageClient
*/
public $client;
/**
* @param LanguageClient $client
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -41,12 +41,12 @@ class ClientHandler
{
$id = $this->idGenerator->generate();
return $this->protocolWriter->write(
new Protocol\Message(
new Message(
new AdvancedJsonRpc\Request($id, $method, (object)$params)
)
)->then(function () use ($id) {
$promise = new Promise;
$listener = function (Protocol\Message $msg) use ($id, $promise, &$listener) {
$listener = function (Message $msg) use ($id, $promise, &$listener) {
if (AdvancedJsonRpc\Response::isResponse($msg->body) && $msg->body->id === $id) {
// Received a response
$this->protocolReader->removeListener('message', $listener);
@ -71,9 +71,8 @@ class ClientHandler
*/
public function notify(string $method, $params): Promise
{
$id = $this->idGenerator->generate();
return $this->protocolWriter->write(
new Protocol\Message(
new Message(
new AdvancedJsonRpc\Notification($method, (object)$params)
)
);

View File

@ -3,15 +3,28 @@ declare(strict_types = 1);
namespace LanguageServer;
use PhpParser\Node;
use LanguageServer\Index\ReadableIndex;
use LanguageServer\Protocol\{
use LanguageServer\Factory\CompletionItemFactory;
use LanguageServerProtocol\{
TextEdit,
Range,
Position,
CompletionList,
CompletionItem,
CompletionItemKind
CompletionItemKind,
CompletionContext,
CompletionTriggerKind
};
use Microsoft\PhpParser;
use Microsoft\PhpParser\Node;
use Microsoft\PhpParser\ResolvedName;
use Generator;
use function LanguageServer\FqnUtilities\{
nameConcat,
nameGetFirstPart,
nameGetParent,
nameStartsWith,
nameWithoutFirstPart
};
class CompletionProvider
@ -48,6 +61,7 @@ class CompletionProvider
'eval',
'exit',
'extends',
'false',
'final',
'finally',
'for',
@ -66,6 +80,7 @@ class CompletionProvider
'list',
'namespace',
'new',
'null',
'or',
'print',
'private',
@ -78,13 +93,30 @@ class CompletionProvider
'switch',
'throw',
'trait',
'true',
'try',
'unset',
'use',
'var',
'while',
'xor',
'yield'
'yield',
// List of other reserved words (http://php.net/manual/en/reserved.other-reserved-words.php)
// (the ones which do not occur as actual keywords above.)
'int',
'float',
'bool',
'string',
'void',
'iterable',
'object',
// Pseudo keywords
'from', // As in yield from
'strict_types',
'ticks', // As in declare(ticks=1)
'encoding', // As in declare(encoding='EBCDIC')
];
/**
@ -104,7 +136,7 @@ class CompletionProvider
/**
* @param DefinitionResolver $definitionResolver
* @param ReadableIndex $index
* @param ReadableIndex $index
*/
public function __construct(DefinitionResolver $definitionResolver, ReadableIndex $index)
{
@ -117,159 +149,85 @@ class CompletionProvider
*
* @param PhpDocument $doc The opened document
* @param Position $pos The cursor position
* @param CompletionContext $context The completion context
* @return CompletionList
*/
public function provideCompletion(PhpDocument $doc, Position $pos): CompletionList
{
public function provideCompletion(
PhpDocument $doc,
Position $pos,
CompletionContext $context = null
): CompletionList {
// This can be made much more performant if the tree follows specific invariants.
$node = $doc->getNodeAtPosition($pos);
if ($node instanceof Node\Expr\Error) {
$node = $node->getAttribute('parentNode');
// Get the node at the position under the cursor
$offset = $node === null ? -1 : $pos->toOffset($node->getFileContents());
if (
$node !== null
&& $offset > $node->getEndPosition()
&& $node->parent !== null
&& $node->parent->getLastChild() instanceof PhpParser\MissingToken
) {
$node = $node->parent;
}
$list = new CompletionList;
$list->isIncomplete = true;
// A non-free node means we do NOT suggest global symbols
if (
$node instanceof Node\Expr\MethodCall
|| $node instanceof Node\Expr\PropertyFetch
|| $node instanceof Node\Expr\StaticCall
|| $node instanceof Node\Expr\StaticPropertyFetch
|| $node instanceof Node\Expr\ClassConstFetch
if ($node instanceof Node\Expression\Variable &&
$node->parent instanceof Node\Expression\ObjectCreationExpression &&
$node->name instanceof PhpParser\MissingToken
) {
// If the name is an Error node, just filter by the class
if ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\PropertyFetch) {
// For instances, resolve the variable type
$prefixes = DefinitionResolver::getFqnsFromType(
$this->definitionResolver->resolveExpressionNodeToType($node->var)
);
} else {
// Static member reference
$prefixes = [$node->class instanceof Node\Name ? (string)$node->class : ''];
}
$prefixes = $this->expandParentFqns($prefixes);
// If we are just filtering by the class, add the appropiate operator to the prefix
// to filter the type of symbol
foreach ($prefixes as &$prefix) {
if ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\PropertyFetch) {
$prefix .= '->';
} else if ($node instanceof Node\Expr\StaticCall || $node instanceof Node\Expr\ClassConstFetch) {
$prefix .= '::';
} else if ($node instanceof Node\Expr\StaticPropertyFetch) {
$prefix .= '::$';
}
}
unset($prefix);
$node = $node->parent;
}
foreach ($this->index->getDefinitions() as $fqn => $def) {
foreach ($prefixes as $prefix) {
if (substr($fqn, 0, strlen($prefix)) === $prefix && !$def->isGlobal) {
$list->items[] = CompletionItem::fromDefinition($def);
}
}
}
} else if (
// A ConstFetch means any static reference, like a class, interface, etc. or keyword
($node instanceof Node\Name && $node->getAttribute('parentNode') instanceof Node\Expr\ConstFetch)
|| $node instanceof Node\Expr\New_
) {
$prefix = '';
$prefixLen = 0;
if ($node instanceof Node\Name) {
$isFullyQualified = $node->isFullyQualified();
$prefix = (string)$node;
$prefixLen = strlen($prefix);
$namespacedPrefix = (string)$node->getAttribute('namespacedName');
$namespacedPrefixLen = strlen($prefix);
}
// Find closest namespace
$namespace = getClosestNode($node, Node\Stmt\Namespace_::class);
/** Map from alias to Definition */
$aliasedDefs = [];
if ($namespace) {
foreach ($namespace->stmts as $stmt) {
if ($stmt instanceof Node\Stmt\Use_ || $stmt instanceof Node\Stmt\GroupUse) {
foreach ($stmt->uses as $use) {
// Get the definition for the used namespace, class-like, function or constant
// And save it under the alias
$fqn = (string)Node\Name::concat($stmt->prefix ?? null, $use->name);
if ($def = $this->index->getDefinition($fqn)) {
$aliasedDefs[$use->alias] = $def;
}
}
} else {
// Use statements are always the first statements in a namespace
break;
}
}
}
// If there is a prefix that does not start with a slash, suggest `use`d symbols
if ($prefix && !$isFullyQualified) {
// Suggest symbols that have been `use`d
// Search the aliases for the typed-in name
foreach ($aliasedDefs as $alias => $def) {
if (substr($alias, 0, $prefixLen) === $prefix) {
$list->items[] = CompletionItem::fromDefinition($def);
}
}
}
// Additionally, suggest global symbols that either
// - start with the current namespace + prefix, if the Name node is not fully qualified
// - start with just the prefix, if the Name node is fully qualified
foreach ($this->index->getDefinitions() as $fqn => $def) {
if (
$def->isGlobal // exclude methods, properties etc.
// 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
&& (
!$prefix
|| (
((!$namespace || $isFullyQualified) && substr($fqn, 0, $prefixLen) === $prefix)
|| (
$namespace
&& !$isFullyQualified
&& substr($fqn, 0, $namespacedPrefixLen) === $namespacedPrefix
)
)
$context->triggerKind === CompletionTriggerKind::INVOKED
|| $context->triggerCharacter === '<'
)
// Only suggest classes for `new`
&& (!($node instanceof Node\Expr\New_) || $def->canBeInstantiated)
) {
$item = CompletionItem::fromDefinition($def);
// Find the shortest name to reference the symbol
if ($namespace && ($alias = array_search($def, $aliasedDefs, true)) !== false) {
// $alias is the name under which this definition is aliased in the current namespace
$item->insertText = $alias;
} else if ($namespace && !($prefix && $isFullyQualified)) {
// Insert the global FQN with trailing backslash
$item->insertText = '\\' . $fqn;
} else {
// Insert the FQN without trailing backlash
$item->insertText = $fqn;
}
$list->items[] = $item;
}
}
// Suggest keywords
if ($node instanceof Node\Name && $node->getAttribute('parentNode') instanceof Node\Expr\ConstFetch) {
foreach (self::KEYWORDS as $keyword) {
if (substr($keyword, 0, $prefixLen) === $prefix) {
$item = new CompletionItem($keyword, CompletionItemKind::KEYWORD);
$item->insertText = $keyword . ' ';
$list->items[] = $item;
}
}
}
} else if (
$node instanceof Node\Expr\Variable
|| ($node && $node->getAttribute('parentNode') instanceof Node\Expr\Variable)
) {
)
)
|| $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
// If there was only a $ typed, $node will be instanceof Node\Error
$namePrefix = $node instanceof Node\Expr\Variable && is_string($node->name) ? $node->name : '';
$namePrefix = $node->getName() ?? '';
foreach ($this->suggestVariablesAtNode($node, $namePrefix) as $var) {
$item = new CompletionItem;
$item->kind = CompletionItemKind::VARIABLE;
$item->label = '$' . ($var instanceof Node\Expr\ClosureUse ? $var->var : $var->name);
$item->label = '$' . $var->getName();
$item->documentation = $this->definitionResolver->getDocumentationFromNode($var);
$item->detail = (string)$this->definitionResolver->getTypeFromNode($var);
$item->textEdit = new TextEdit(
@ -278,36 +236,365 @@ class CompletionProvider
);
$list->items[] = $item;
}
} else if ($node instanceof Node\Stmt\InlineHTML || $pos == new Position(0, 0)) {
$item = new CompletionItem('<?php', CompletionItemKind::KEYWORD);
$item->textEdit = new TextEdit(
new Range($pos, $pos),
stripStringOverlap($doc->getRange(new Range(new Position(0, 0), $pos)), '<?php')
);
$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
);
}
}
/**
* Adds the FQNs of all parent classes to an array of FQNs of classes
* Yields completions for non-qualified global names.
*
* Yields
* - Aliased classes
* - Completions from current namespace
* - Roamed completions from the global namespace (when not creating and not already in root NS)
* - PHP keywords (when not creating)
*
* @return \Generator|CompletionItem[]
* Yields CompletionItems
*/
private function getUnqualifiedCompletions(
string $prefix,
string $currentNamespace,
array $importTables,
bool $requireCanBeInstantiated
): \Generator {
// Aliases
list($namespaceAliases,,) = $importTables;
// use Foo\Bar
yield from $this->getCompletionsForAliases(
$prefix,
$namespaceAliases,
$requireCanBeInstantiated
);
// Completions from the current namespace
yield from $this->getCompletionsForFqnPrefix(
nameConcat($currentNamespace, $prefix),
$requireCanBeInstantiated,
false
);
if ($currentNamespace !== '' && $prefix === '') {
// Get additional suggestions from the global namespace.
// When completing e.g. for new |, suggest \DateTime
yield from $this->getCompletionsForFqnPrefix('', $requireCanBeInstantiated, true);
}
if (!$requireCanBeInstantiated) {
if ($currentNamespace !== '' && $prefix !== '') {
// Roamed definitions (i.e. global constants and functions). The prefix is checked against '', since
// in that case global completions have already been provided (including non-roamed definitions.)
yield from $this->getRoamedCompletions($prefix);
}
// Lastly and least importantly, suggest keywords.
yield from $this->getCompletionsForKeywords($prefix);
}
}
/**
* Gets completions for prefixes of fully qualified names in their parent namespace.
*
* @param string $prefix Prefix to complete for. Fully qualified.
* @param bool $requireCanBeInstantiated If set, only return classes.
* @param bool $insertFullyQualified If set, return completion with the leading \ inserted.
* @return \Generator|CompletionItem[]
* Yields CompletionItems.
*/
private function getCompletionsForFqnPrefix(
string $prefix,
bool $requireCanBeInstantiated,
bool $insertFullyQualified
): \Generator {
$namespace = nameGetParent($prefix);
foreach ($this->index->getChildDefinitionsForFqn($namespace) as $fqn => $def) {
if ($requireCanBeInstantiated && !$def->canBeInstantiated) {
continue;
}
if (!nameStartsWith($fqn, $prefix)) {
continue;
}
$completion = CompletionItemFactory::fromDefinition($def);
if ($insertFullyQualified) {
$completion->insertText = '\\' . $fqn;
}
yield $fqn => $completion;
}
}
/**
* Gets completions for non-qualified names matching the start of an used class, function, or constant.
*
* @param string $prefix Non-qualified name being completed for
* @param QualifiedName[] $aliases Array of alias FQNs indexed by the alias.
* @return \Generator|CompletionItem[]
* Yields CompletionItems.
*/
private function getCompletionsForAliases(
string $prefix,
array $aliases,
bool $requireCanBeInstantiated
): \Generator {
foreach ($aliases as $alias => $aliasFqn) {
if (!nameStartsWith($alias, $prefix)) {
continue;
}
$definition = $this->index->getDefinition((string)$aliasFqn);
if ($definition) {
if ($requireCanBeInstantiated && !$definition->canBeInstantiated) {
continue;
}
$completionItem = CompletionItemFactory::fromDefinition($definition);
$completionItem->insertText = $alias;
yield (string)$aliasFqn => $completionItem;
}
}
}
/**
* Gets completions for partially qualified names, where the first part is matched by an alias.
*
* @return \Generator|CompletionItem[]
* Yields CompletionItems.
*/
private function getCompletionsFromAliasedNamespace(
string $prefix,
string $alias,
string $aliasFqn,
bool $requireCanBeInstantiated
): \Generator {
$prefixFirstPart = nameGetFirstPart($prefix);
// Matched alias.
$resolvedPrefix = nameConcat($aliasFqn, nameWithoutFirstPart($prefix));
$completionItems = $this->getCompletionsForFqnPrefix(
$resolvedPrefix,
$requireCanBeInstantiated,
false
);
// Convert FQNs in the CompletionItems so they are expressed in terms of the alias.
foreach ($completionItems as $fqn => $completionItem) {
/** @var string $fqn with the leading parts determined by the alias removed. Has the leading backslash. */
$nameWithoutAliasedPart = substr($fqn, strlen($aliasFqn));
$completionItem->insertText = $alias . $nameWithoutAliasedPart;
yield $fqn => $completionItem;
}
}
/**
* Gets completions for globally defined functions and constants (i.e. symbols which may be used anywhere)
*
* @return \Generator|CompletionItem[]
* Yields CompletionItems.
*/
private function getRoamedCompletions(string $prefix): \Generator
{
foreach ($this->index->getChildDefinitionsForFqn('') as $fqn => $def) {
if (!$def->roamed || !nameStartsWith($fqn, $prefix)) {
continue;
}
$completionItem = CompletionItemFactory::fromDefinition($def);
// Second-guessing the user here - do not trust roaming to work. If the same symbol is
// inserted in the current namespace, the code will stop working.
$completionItem->insertText = '\\' . $fqn;
yield $fqn => $completionItem;
}
}
/**
* Completes PHP keywords.
*
* @return \Generator|CompletionItem[]
* Yields CompletionItems.
*/
private function getCompletionsForKeywords(string $prefix): \Generator
{
foreach (self::KEYWORDS as $keyword) {
if (nameStartsWith($keyword, $prefix)) {
$item = new CompletionItem($keyword, CompletionItemKind::KEYWORD);
$item->insertText = $keyword;
yield $keyword => $item;
}
}
}
/**
* Yields FQNs from an array along with the FQNs of all parent classes
*
* @param string[] $fqns
* @return string[]
* @return Generator
*/
private function expandParentFqns(array $fqns): array
private function expandParentFqns(array $fqns) : Generator
{
$expanded = $fqns;
foreach ($fqns as $fqn) {
yield $fqn;
$def = $this->index->getDefinition($fqn);
if ($def) {
foreach ($this->expandParentFqns($def->extends) as $parent) {
$expanded[] = $parent;
if ($def !== null) {
foreach ($def->getAncestorDefinitions($this->index) as $name => $def) {
yield $name;
}
}
}
return $expanded;
}
/**
@ -335,30 +622,35 @@ class CompletionProvider
// Walk the AST upwards until a scope boundary is met
$level = $node;
while ($level && !($level instanceof Node\FunctionLike)) {
while ($level && !($level instanceof PhpParser\FunctionLike)) {
// Walk siblings before the node
$sibling = $level;
while ($sibling = $sibling->getAttribute('previousSibling')) {
while ($sibling = $sibling->getPreviousSibling()) {
// Collect all variables inside the sibling node
foreach ($this->findVariableDefinitionsInNode($sibling, $namePrefix) as $var) {
$vars[$var->name] = $var;
$vars[$var->getName()] = $var;
}
}
$level = $level->getAttribute('parentNode');
$level = $level->parent;
}
// If the traversal ended because a function was met,
// also add its parameters and closure uses to the result list
if ($level instanceof Node\FunctionLike) {
foreach ($level->params as $param) {
if (!isset($vars[$param->name]) && substr($param->name, 0, strlen($namePrefix)) === $namePrefix) {
$vars[$param->name] = $param;
if ($level && $level instanceof PhpParser\FunctionLike && $level->parameters !== null) {
foreach ($level->parameters->getValues() as $param) {
$paramName = $param->getName();
if (empty($namePrefix) || strpos($paramName, $namePrefix) !== false) {
$vars[$paramName] = $param;
}
}
if ($level instanceof Node\Expr\Closure) {
foreach ($level->uses as $use) {
if (!isset($vars[$use->var]) && substr($use->var, 0, strlen($namePrefix)) === $namePrefix) {
$vars[$use->var] = $use;
if ($level instanceof Node\Expression\AnonymousFunctionCreationExpression
&& $level->anonymousFunctionUseClause !== null
&& $level->anonymousFunctionUseClause->useVariableNameList !== null) {
foreach ($level->anonymousFunctionUseClause->useVariableNameList->getValues() as $use) {
$useName = $use->getName();
if (empty($namePrefix) || strpos($useName, $namePrefix) !== false) {
$vars[$useName] = $use;
}
}
}
@ -372,38 +664,44 @@ class CompletionProvider
*
* @param Node $node
* @param string $namePrefix Prefix to filter
* @return Node\Expr\Variable[]
* @return Node\Expression\Variable[]
*/
private function findVariableDefinitionsInNode(Node $node, string $namePrefix = ''): array
{
$vars = [];
// If the child node is a variable assignment, save it
$parent = $node->getAttribute('parentNode');
if (
$node instanceof Node\Expr\Variable
&& ($parent instanceof Node\Expr\Assign || $parent instanceof Node\Expr\AssignOp)
&& is_string($node->name) // Variable variables are of no use
&& substr($node->name, 0, strlen($namePrefix)) === $namePrefix
) {
$vars[] = $node;
}
// Iterate over subnodes
foreach ($node->getSubNodeNames() as $attr) {
if (!isset($node->$attr)) {
continue;
}
$children = is_array($node->$attr) ? $node->$attr : [$node->$attr];
foreach ($children as $child) {
// Dont try to traverse scalars
// Dont traverse functions, the contained variables are in a different scope
if (!($child instanceof Node) || $child instanceof Node\FunctionLike) {
continue;
$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;
}
foreach ($this->findVariableDefinitionsInNode($child, $namePrefix) as $var) {
$vars[] = $var;
}
} else {
// Get all descendent variables, then filter to ones that start with $namePrefix.
// Avoiding closure usage in tight loop
foreach ($node->getDescendantNodes($isAssignmentToVariable) as $descendantNode) {
if ($this->isAssignmentToVariableWithPrefix($descendantNode, $namePrefix)) {
$vars[] = $descendantNode->leftOperand;
}
}
}
return $vars;
}
private function isAssignmentToVariableWithPrefix(Node $node, string $namePrefix): bool
{
return $node instanceof Node\Expression\AssignmentExpression
&& $node->leftOperand instanceof Node\Expression\Variable
&& ($namePrefix === '' || strpos($node->leftOperand->getName(), $namePrefix) !== false);
}
}

View File

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

View File

@ -4,7 +4,7 @@ declare(strict_types = 1);
namespace LanguageServer\ContentRetriever;
use LanguageServer\LanguageClient;
use LanguageServer\Protocol\{TextDocumentIdentifier, TextDocumentItem};
use LanguageServerProtocol\{TextDocumentIdentifier, TextDocumentItem};
use Sabre\Event\Promise;
/**

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -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;
}
}

View File

@ -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)
));
}
}

View File

@ -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)
);
}
}

View File

@ -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;
}
}

View File

@ -3,7 +3,6 @@ declare(strict_types = 1);
namespace LanguageServer\FilesFinder;
use Webmozart\Glob\Iterator\GlobIterator;
use Sabre\Event\Promise;
use function Sabre\Event\coroutine;
use function LanguageServer\{pathToUri, timeout};
@ -22,7 +21,11 @@ class FileSystemFilesFinder implements FilesFinder
return coroutine(function () use ($glob) {
$uris = [];
foreach (new GlobIterator($glob) as $path) {
$uris[] = pathToUri($path);
// Exclude any directories that also match the glob pattern
if (!is_dir($path) || !is_readable($path)) {
$uris[] = pathToUri($path);
}
yield timeout();
}
return $uris;

View File

@ -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);
}
}

View File

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

118
src/FqnUtilities.php Normal file
View File

@ -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;
}

View File

@ -99,20 +99,29 @@ abstract class AbstractAggregateIndex implements ReadableIndex
}
/**
* Returns an associative array [string => Definition] that maps fully qualified symbol names
* to Definitions
* Returns a Generator providing an associative array [string => Definition]
* that maps fully qualified symbol names to Definitions (global or not)
*
* @return Definition[]
* @return \Generator yields Definition
*/
public function getDefinitions(): array
public function getDefinitions(): \Generator
{
$defs = [];
foreach ($this->getIndexes() as $index) {
foreach ($index->getDefinitions() as $fqn => $def) {
$defs[$fqn] = $def;
}
yield from $index->getDefinitions();
}
}
/**
* Returns a Generator that yields all the direct child Definitions of a given FQN
*
* @param string $fqn
* @return \Generator yields Definition
*/
public function getChildDefinitionsForFqn(string $fqn): \Generator
{
foreach ($this->getIndexes() as $index) {
yield from $index->getChildDefinitionsForFqn($fqn);
}
return $defs;
}
/**
@ -132,19 +141,15 @@ abstract class AbstractAggregateIndex implements ReadableIndex
}
/**
* Returns all URIs in this index that reference a symbol
* Returns a Generator providing all URIs in this index that reference a symbol
*
* @param string $fqn The fully qualified name of the symbol
* @return string[]
* @return \Generator yields string
*/
public function getReferenceUris(string $fqn): array
public function getReferenceUris(string $fqn): \Generator
{
$refs = [];
foreach ($this->getIndexes() as $index) {
foreach ($index->getReferenceUris($fqn) as $ref) {
$refs[] = $ref;
}
yield from $index->getReferenceUris($fqn);
}
return $refs;
}
}

View File

@ -15,14 +15,26 @@ class Index implements ReadableIndex, \Serializable
use EmitterTrait;
/**
* An associative array that maps fully qualified symbol names to Definitions
* An associative array that maps splitted fully qualified symbol names
* to definitions, eg :
* [
* 'Psr' => [
* '\Log' => [
* '\LoggerInterface' => [
* '' => $def1, // definition for 'Psr\Log\LoggerInterface' which is non-member
* '->log()' => $def2, // definition for 'Psr\Log\LoggerInterface->log()' which is a member
* ],
* ],
* ],
* ]
*
* @var Definition[]
* @var array
*/
private $definitions = [];
/**
* An associative array that maps fully qualified symbol names to arrays of document URIs that reference the symbol
* An associative array that maps fully qualified symbol names
* to arrays of document URIs that reference the symbol
*
* @var string[][]
*/
@ -84,14 +96,46 @@ class Index implements ReadableIndex, \Serializable
}
/**
* Returns an associative array [string => Definition] that maps fully qualified symbol names
* to Definitions
* Returns a Generator providing an associative array [string => Definition]
* that maps fully qualified symbol names to Definitions (global or not)
*
* @return Definition[]
* @return \Generator yields Definition
*/
public function getDefinitions(): array
public function getDefinitions(): \Generator
{
return $this->definitions;
yield from $this->yieldDefinitionsRecursively($this->definitions);
}
/**
* Returns a Generator that yields all the direct child Definitions of a given FQN
*
* @param string $fqn
* @return \Generator yields Definition
*/
public function getChildDefinitionsForFqn(string $fqn): \Generator
{
$parts = $this->splitFqn($fqn);
if ('' === end($parts)) {
// we want to return all the definitions in the given FQN, not only
// the one (non member) matching exactly the FQN.
array_pop($parts);
}
$result = $this->getIndexValue($parts, $this->definitions);
if (!$result) {
return;
}
foreach ($result as $name => $item) {
// Don't yield the parent
if ($name === '') {
continue;
}
if ($item instanceof Definition) {
yield $fqn.$name => $item;
} elseif (is_array($item) && isset($item[''])) {
yield $fqn.$name => $item[''];
}
}
}
/**
@ -103,12 +147,17 @@ class Index implements ReadableIndex, \Serializable
*/
public function getDefinition(string $fqn, bool $globalFallback = false)
{
if (isset($this->definitions[$fqn])) {
return $this->definitions[$fqn];
$parts = $this->splitFqn($fqn);
$result = $this->getIndexValue($parts, $this->definitions);
if ($result instanceof Definition) {
return $result;
}
if ($globalFallback) {
$parts = explode('\\', $fqn);
$fqn = end($parts);
return $this->getDefinition($fqn);
}
}
@ -117,12 +166,14 @@ class Index implements ReadableIndex, \Serializable
* Registers a definition
*
* @param string $fqn The fully qualified name of the symbol
* @param string $definition The Definition object
* @param Definition $definition The Definition object
* @return void
*/
public function setDefinition(string $fqn, Definition $definition)
{
$this->definitions[$fqn] = $definition;
$parts = $this->splitFqn($fqn);
$this->indexDefinition(0, $parts, $this->definitions, $definition);
$this->emit('definition-added');
}
@ -135,19 +186,34 @@ class Index implements ReadableIndex, \Serializable
*/
public function removeDefinition(string $fqn)
{
unset($this->definitions[$fqn]);
$parts = $this->splitFqn($fqn);
$this->removeIndexedDefinition(0, $parts, $this->definitions, $this->definitions);
unset($this->references[$fqn]);
}
/**
* Returns all URIs in this index that reference a symbol
* Returns a Generator providing all URIs in this index that reference a symbol
*
* @param string $fqn The fully qualified name of the symbol
* @return string[]
* @return \Generator yields string
*/
public function getReferenceUris(string $fqn): array
public function getReferenceUris(string $fqn): \Generator
{
return $this->references[$fqn] ?? [];
foreach ($this->references[$fqn] ?? [] as $uri) {
yield $uri;
}
}
/**
* For test use.
* Returns all references, keyed by fqn.
*
* @return string[][]
*/
public function getReferences(): array
{
return $this->references;
}
/**
@ -193,22 +259,184 @@ class Index implements ReadableIndex, \Serializable
public function unserialize($serialized)
{
$data = unserialize($serialized);
if (isset($data['definitions'])) {
foreach ($data['definitions'] as $fqn => $definition) {
$this->setDefinition($fqn, $definition);
}
unset($data['definitions']);
}
foreach ($data as $prop => $val) {
$this->$prop = $val;
}
}
/**
* @param string $serialized
* @return string
*/
public function serialize()
{
return serialize([
'definitions' => $this->definitions,
'definitions' => iterator_to_array($this->getDefinitions()),
'references' => $this->references,
'complete' => $this->complete,
'staticComplete' => $this->staticComplete
]);
}
/**
* Returns a Generator that yields all the Definitions in the given $storage recursively.
* The generator yields key => value pairs, e.g.
* `'Psr\Log\LoggerInterface->log()' => $definition`
*
* @param array &$storage
* @param string $prefix (optional)
* @return \Generator
*/
private function yieldDefinitionsRecursively(array &$storage, string $prefix = ''): \Generator
{
foreach ($storage as $key => $value) {
if (!is_array($value)) {
yield $prefix.$key => $value;
} else {
yield from $this->yieldDefinitionsRecursively($value, $prefix.$key);
}
}
}
/**
* Splits the given FQN into an array, eg :
* - `'Psr\Log\LoggerInterface->log'` will be `['Psr', '\Log', '\LoggerInterface', '->log()']`
* - `'\Exception->getMessage()'` will be `['\Exception', '->getMessage()']`
* - `'PHP_VERSION'` will be `['PHP_VERSION']`
*
* @param string $fqn
* @return string[]
*/
private function splitFqn(string $fqn): array
{
// split fqn at backslashes
$parts = explode('\\', $fqn);
// write back the backslash prefix to the first part if it was present
if ('' === $parts[0] && count($parts) > 1) {
$parts = array_slice($parts, 1);
$parts[0] = '\\' . $parts[0];
}
// write back the backslashes prefixes for the other parts
for ($i = 1; $i < count($parts); $i++) {
$parts[$i] = '\\' . $parts[$i];
}
// split the last part in 2 parts at the operator
$hasOperator = false;
$lastPart = end($parts);
foreach (['::', '->'] as $operator) {
$endParts = explode($operator, $lastPart);
if (count($endParts) > 1) {
$hasOperator = true;
// replace the last part by its pieces
array_pop($parts);
$parts[] = $endParts[0];
$parts[] = $operator . $endParts[1];
break;
}
}
// The end($parts) === '' holds for the root namespace.
if (!$hasOperator && end($parts) !== '') {
// add an empty part to store the non-member definition to avoid
// definition collisions in the index array, eg
// 'Psr\Log\LoggerInterface' will be stored at
// ['Psr']['\Log']['\LoggerInterface'][''] to be able to also store
// member definitions, ie 'Psr\Log\LoggerInterface->log()' will be
// stored at ['Psr']['\Log']['\LoggerInterface']['->log()']
$parts[] = '';
}
return $parts;
}
/**
* Return the values stored in this index under the given $parts array.
* It can be an index node or a Definition if the $parts are precise
* enough. Returns null when nothing is found.
*
* @param string[] $path The splitted FQN
* @param array|Definition &$storage The current level to look for $path.
* @return array|Definition|null
*/
private function getIndexValue(array $path, &$storage)
{
// Empty path returns the object itself.
if (empty($path)) {
return $storage;
}
$part = array_shift($path);
if (!isset($storage[$part])) {
return null;
}
return $this->getIndexValue($path, $storage[$part]);
}
/**
* Recursive function that stores the given Definition in the given $storage array represented
* as a tree matching the given $parts.
*
* @param int $level The current level of FQN part
* @param string[] $parts The splitted FQN
* @param array &$storage The array in which to store the $definition
* @param Definition $definition The Definition to store
*/
private function indexDefinition(int $level, array $parts, array &$storage, Definition $definition)
{
$part = $parts[$level];
if ($level + 1 === count($parts)) {
$storage[$part] = $definition;
return;
}
if (!isset($storage[$part])) {
$storage[$part] = [];
}
$this->indexDefinition($level + 1, $parts, $storage[$part], $definition);
}
/**
* Recursive function that removes the definition matching the given $parts from the given
* $storage array. The function also looks up recursively to remove the parents of the
* definition which no longer has children to avoid to let empty arrays in the index.
*
* @param int $level The current level of FQN part
* @param string[] $parts The splitted FQN
* @param array &$storage The current array in which to remove data
* @param array &$rootStorage The root storage array
*/
private function removeIndexedDefinition(int $level, array $parts, array &$storage, array &$rootStorage)
{
$part = $parts[$level];
if ($level + 1 === count($parts)) {
if (isset($storage[$part])) {
unset($storage[$part]);
if (0 === count($storage)) {
// parse again the definition tree to remove the parent
// when it has no more children
$this->removeIndexedDefinition(0, array_slice($parts, 0, $level), $rootStorage, $rootStorage);
}
}
} else {
$this->removeIndexedDefinition($level + 1, $parts, $storage[$part], $rootStorage);
}
}
}

View File

@ -30,12 +30,20 @@ interface ReadableIndex extends EmitterInterface
public function isStaticComplete(): bool;
/**
* Returns an associative array [string => Definition] that maps fully qualified symbol names
* to Definitions
* Returns a Generator providing an associative array [string => Definition]
* that maps fully qualified symbol names to Definitions (global or not)
*
* @return Definitions[]
* @return \Generator yields Definition
*/
public function getDefinitions(): array;
public function getDefinitions(): \Generator;
/**
* Returns a Generator that yields all the direct child Definitions of a given FQN
*
* @param string $fqn
* @return \Generator yields Definition
*/
public function getChildDefinitionsForFqn(string $fqn): \Generator;
/**
* Returns the Definition object by a specific FQN
@ -47,10 +55,10 @@ interface ReadableIndex extends EmitterInterface
public function getDefinition(string $fqn, bool $globalFallback = false);
/**
* Returns all URIs in this index that reference a symbol
* Returns a Generator that yields all URIs in this index that reference a symbol
*
* @param string $fqn The fully qualified name of the symbol
* @return string[]
* @return \Generator yields string
*/
public function getReferenceUris(string $fqn): array;
public function getReferenceUris(string $fqn): \Generator;
}

View File

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

View File

@ -3,28 +3,24 @@ declare(strict_types = 1);
namespace LanguageServer;
use LanguageServer\Protocol\{
use LanguageServerProtocol\{
ServerCapabilities,
ClientCapabilities,
TextDocumentSyncKind,
Message,
MessageType,
InitializeResult,
SymbolInformation,
TextDocumentIdentifier,
CompletionOptions
CompletionOptions,
SignatureHelpOptions
};
use LanguageServer\Message;
use LanguageServer\FilesFinder\{FilesFinder, ClientFilesFinder, FileSystemFilesFinder};
use LanguageServer\ContentRetriever\{ContentRetriever, ClientContentRetriever, FileSystemContentRetriever};
use LanguageServer\Index\{DependenciesIndex, GlobalIndex, Index, ProjectIndex, StubsIndex};
use LanguageServer\Cache\{FileSystemCache, ClientCache};
use AdvancedJsonRpc;
use Sabre\Event\{Loop, Promise};
use Sabre\Event\Promise;
use function Sabre\Event\coroutine;
use Exception;
use Throwable;
use Webmozart\PathUtil\Path;
use Sabre\Uri;
class LanguageServer extends AdvancedJsonRpc\Dispatcher
{
@ -111,7 +107,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
protected $definitionResolver;
/**
* @param PotocolReader $reader
* @param ProtocolReader $reader
* @param ProtocolWriter $writer
*/
public function __construct(ProtocolReader $reader, ProtocolWriter $writer)
@ -137,7 +133,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
// If a ResponseError is thrown, send it back in the Response
$error = $e;
} catch (Throwable $e) {
// If an unexpected error occured, send back an INTERNAL_ERROR error response
// If an unexpected error occurred, send back an INTERNAL_ERROR error response
$error = new AdvancedJsonRpc\Error(
(string)$e,
AdvancedJsonRpc\ErrorCode::INTERNAL_ERROR,
@ -169,8 +165,11 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
* @param int|null $processId The process Id of the parent process that started the server. Is null if the process has not been started by another process. If the parent process is not alive then the server should exit (see exit notification) its process.
* @return Promise <InitializeResult>
*/
public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null): Promise
public function initialize(ClientCapabilities $capabilities, string $rootPath = null, int $processId = null, string $rootUri = null): Promise
{
if ($rootPath === null && $rootUri !== null) {
$rootPath = uriToPath($rootUri);
}
return coroutine(function () use ($capabilities, $rootPath, $processId) {
if ($capabilities->xfilesProvider) {
@ -270,8 +269,6 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
$serverCapabilities->documentSymbolProvider = true;
// Support "Find all symbols in workspace"
$serverCapabilities->workspaceSymbolProvider = true;
// Support "Format Code"
$serverCapabilities->documentFormattingProvider = true;
// Support "Go to definition"
$serverCapabilities->definitionProvider = true;
// Support "Find all references"
@ -282,6 +279,10 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
$serverCapabilities->completionProvider = new CompletionOptions;
$serverCapabilities->completionProvider->resolveProvider = false;
$serverCapabilities->completionProvider->triggerCharacters = ['$', '>'];
$serverCapabilities->signatureHelpProvider = new SignatureHelpOptions();
$serverCapabilities->signatureHelpProvider->triggerCharacters = ['(', ','];
// Support global references
$serverCapabilities->xworkspaceReferencesProvider = true;
$serverCapabilities->xdefinitionProvider = true;

View File

@ -1,9 +1,10 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Protocol;
namespace LanguageServer;
use AdvancedJsonRpc\Message as MessageBody;
use LanguageServer\Message;
class Message
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

116
src/ParserHelpers.php Normal file
View File

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

View File

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

View File

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

View File

@ -1,27 +0,0 @@
<?php
namespace LanguageServer\Protocol;
class ClientCapabilities
{
/**
* The client supports workspace/xfiles requests
*
* @var bool|null
*/
public $xfilesProvider;
/**
* The client supports textDocument/xcontent requests
*
* @var bool|null
*/
public $xcontentProvider;
/**
* The client supports xcache/* requests
*
* @var bool|null
*/
public $xcacheProvider;
}

View File

@ -1,17 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* Contains additional diagnostic information about the context in which
* a code action is run.
*/
class CodeActionContext
{
/**
* An array of diagnostics.
*
* @var Diagnostic[]
*/
public $diagnostics;
}

View File

@ -1,35 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* A code lens represents a command that should be shown along with
* source text, like the number of references, a way to run tests, etc.
*
* A code lens is _unresolved_ when no command is associated to it. For performance
* reasons the creation of a code lens and resolving should be done in two stages.
*/
class CodeLens
{
/**
* The range in which this code lens is valid. Should only span a single line.
*
* @var Range
*/
public $range;
/**
* The command this code lens represents.
*
* @var Command|null
*/
public $command;
/**
* A data entry field that is preserved on a code lens item between
* a code lens and a code lens resolve request.
*
* @var mixed|null
*/
public $data;
}

View File

@ -1,16 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* Code Lens options.
*/
class CodeLensOptions
{
/**
* Code lens has a resolve provider as well.
*
* @var bool|null
*/
public $resolveProvider;
}

View File

@ -1,32 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* Represents a reference to a command. Provides a title which will be used to represent a command in the UI and,
* optionally, an array of arguments which will be passed to the command handler function when invoked.
*/
class Command
{
/**
* Title of the command, like `save`.
*
* @var string
*/
public $title;
/**
* The identifier of the actual command handler.
*
* @var string
*/
public $command;
/**
* Arguments that the command handler should be
* invoked with.
*
* @var mixed[]|null
*/
public $arguments;
}

View File

@ -1,164 +0,0 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Protocol;
use LanguageServer\Definition;
class CompletionItem
{
/**
* The label of this completion item. By default
* also the text that is inserted when selecting
* this completion.
*
* @var string
*/
public $label;
/**
* The kind of this completion item. Based of the kind
* an icon is chosen by the editor.
*
* @var int|null
*/
public $kind;
/**
* A human-readable string with additional information
* about this item, like type or symbol information.
*
* @var string|null
*/
public $detail;
/**
* A human-readable string that represents a doc-comment.
*
* @var string|null
*/
public $documentation;
/**
* A string that shoud be used when comparing this item
* with other items. When `falsy` the label is used.
*
* @var string|null
*/
public $sortText;
/**
* A string that should be used when filtering a set of
* completion items. When `falsy` the label is used.
*
* @var string|null
*/
public $filterText;
/**
* A string that should be inserted a document when selecting
* this completion. When `falsy` the label is used.
*
* @var string|null
*/
public $insertText;
/**
* An edit which is applied to a document when selecting
* this completion. When an edit is provided the value of
* insertText is ignored.
*
* @var TextEdit|null
*/
public $textEdit;
/**
* An optional array of additional text edits that are applied when
* selecting this completion. Edits must not overlap with the main edit
* nor with themselves.
*
* @var TextEdit[]|null
*/
public $additionalTextEdits;
/**
* An optional command that is executed *after* inserting this completion. *Note* that
* additional modifications to the current document should be described with the
* additionalTextEdits-property.
*
* @var Command|null
*/
public $command;
/**
* An data entry field that is preserved on a completion item between
* a completion and a completion resolve request.
*
* @var mixed
*/
public $data;
/**
* @param string $label
* @param int|null $kind
* @param string|null $detail
* @param string|null $documentation
* @param string|null $sortText
* @param string|null $filterText
* @param string|null $insertText
* @param TextEdit|null $textEdit
* @param TextEdit[]|null $additionalTextEdits
* @param Command|null $command
* @param mixed|null $data
*/
public function __construct(
string $label = null,
int $kind = null,
string $detail = null,
string $documentation = null,
string $sortText = null,
string $filterText = null,
string $insertText = null,
TextEdit $textEdit = null,
array $additionalTextEdits = null,
Command $command = null,
$data = null
) {
$this->label = $label;
$this->kind = $kind;
$this->detail = $detail;
$this->documentation = $documentation;
$this->sortText = $sortText;
$this->filterText = $filterText;
$this->insertText = $insertText;
$this->textEdit = $textEdit;
$this->additionalTextEdits = $additionalTextEdits;
$this->command = $command;
$this->data = $data;
}
/**
* Creates a CompletionItem for a Definition
*
* @param Definition $def
* @return self
*/
public static function fromDefinition(Definition $def): self
{
$item = new CompletionItem;
$item->label = $def->symbolInformation->name;
$item->kind = CompletionItemKind::fromSymbolKind($def->symbolInformation->kind);
if ($def->type) {
$item->detail = (string)$def->type;
} else if ($def->symbolInformation->containerName) {
$item->detail = $def->symbolInformation->containerName;
}
if ($def->documentation) {
$item->documentation = $def->documentation;
}
if ($def->isStatic && $def->symbolInformation->kind === SymbolKind::PROPERTY) {
$item->insertText = '$' . $def->symbolInformation->name;
}
return $item;
}
}

View File

@ -1,70 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* The kind of a completion entry.
*/
abstract class CompletionItemKind
{
const TEXT = 1;
const METHOD = 2;
const FUNCTION = 3;
const CONSTRUCTOR = 4;
const FIELD = 5;
const VARIABLE = 6;
const CLASS_ = 7;
const INTERFACE = 8;
const MODULE = 9;
const PROPERTY = 10;
const UNIT = 11;
const VALUE = 12;
const ENUM = 13;
const KEYWORD = 14;
const SNIPPET = 15;
const COLOR = 16;
const FILE = 17;
const REFERENCE = 18;
/**
* Returns the CompletionItemKind for a SymbolKind
*
* @param int $kind A SymbolKind
* @return int The CompletionItemKind
*/
public static function fromSymbolKind(int $kind): int
{
switch ($kind) {
case SymbolKind::PROPERTY:
case SymbolKind::FIELD:
return self::PROPERTY;
case SymbolKind::METHOD:
return self::METHOD;
case SymbolKind::CLASS_:
return self::CLASS_;
case SymbolKind::INTERFACE:
return self::INTERFACE;
case SymbolKind::FUNCTION:
return self::FUNCTION;
case SymbolKind::NAMESPACE:
case SymbolKind::MODULE:
case SymbolKind::PACKAGE:
return self::MODULE;
case SymbolKind::FILE:
return self::FILE;
case SymbolKind::STRING:
return self::TEXT;
case SymbolKind::NUMBER:
case SymbolKind::BOOLEAN:
case SymbolKind::ARRAY:
return self::VALUE;
case SymbolKind::ENUM:
return self::ENUM;
case SymbolKind::CONSTRUCTOR:
return self::CONSTRUCTOR;
case SymbolKind::VARIABLE:
case SymbolKind::CONSTANT:
return self::VARIABLE;
}
}
}

View File

@ -1,35 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* Represents a collection of completion items to be presented in
* the editor.
*/
class CompletionList
{
/**
* This list it not complete. Further typing should result in recomputing this
* list.
*
* @var bool
*/
public $isIncomplete;
/**
* The completion items.
*
* @var CompletionItem[]
*/
public $items;
/**
* @param CompletionItem[] $items The completion items.
* @param bool $isIncomplete This list it not complete. Further typing should result in recomputing this list.
*/
public function __construct(array $items = [], bool $isIncomplete = false)
{
$this->items = $items;
$this->isIncomplete = $isIncomplete;
}
}

View File

@ -1,24 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* Completion options.
*/
class CompletionOptions
{
/*
* The server provides support to resolve additional information for a completion
* item.
*
* @var bool|null
*/
public $resolveProvider;
/**
* The characters that trigger completion automatically.
*
* @var string[]|null
*/
public $triggerCharacters;
}

View File

@ -1,31 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* An event describing a change to a text document. If range and rangeLength are
* omitted the new text is considered to be the full content of the document.
*/
class ContentChangeEvent
{
/**
* The range of the document that changed.
*
* @var Range|null
*/
public $range;
/**
* The length of the range that got replaced.
*
* @var int|null
*/
public $rangeLength;
/**
* The new text of the document.
*
* @var string
*/
public $text;
}

View File

@ -1,27 +0,0 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Protocol;
class DependencyReference
{
/**
* @var mixed
*/
public $hints;
/**
* @var object
*/
public $attributes;
/**
* @param object $attributes
* @param mixed $hints
*/
public function __construct($attributes = null, $hints = null)
{
$this->attributes = $attributes ?? new \stdClass;
$this->hints = $hints;
}
}

View File

@ -1,85 +0,0 @@
<?php
namespace LanguageServer\Protocol;
use PhpParser\Error;
/**
* Represents a diagnostic, such as a compiler error or warning. Diagnostic objects are only valid in the scope of a
* resource.
*/
class Diagnostic
{
/**
* The range at which the message applies.
*
* @var Range
*/
public $range;
/**
* The diagnostic's severity. Can be omitted. If omitted it is up to the
* client to interpret diagnostics as error, warning, info or hint.
*
* @var int|null
*/
public $severity;
/**
* The diagnostic's code. Can be omitted.
*
* @var int|string|null
*/
public $code;
/**
* A human-readable string describing the source of this
* diagnostic, e.g. 'typescript' or 'super lint'.
*
* @var string|null
*/
public $source;
/**
* The diagnostic's message.
*
* @var string
*/
public $message;
/**
* Creates a diagnostic from a PhpParser Error
*
* @param Error $error Message and code will be used
* @param string $content The file content to calculate the column info
* @param int $severity DiagnosticSeverity
* @param string $source A human-readable string describing the source of this diagnostic
* @return self
*/
public static function fromError(Error $error, string $content, int $severity = null, string $source = null): self
{
return new self(
$error->getRawMessage(), // Do not include "on line ..." in the error message
Range::fromError($error, $content),
$error->getCode(),
$severity,
$source
);
}
/**
* @param string $message The diagnostic's message
* @param Range $range The range at which the message applies
* @param int $code The diagnostic's code
* @param int $severity DiagnosticSeverity
* @param string $source A human-readable string describing the source of this diagnostic
*/
public function __construct(string $message = null, Range $range = null, int $code = null, int $severity = null, string $source = null)
{
$this->message = $message;
$this->range = $range;
$this->code = $code;
$this->severity = $severity;
$this->source = $source;
}
}

View File

@ -1,26 +0,0 @@
<?php
namespace LanguageServer\Protocol;
abstract class DiagnosticSeverity
{
/**
* Reports an error.
*/
const ERROR = 1;
/**
* Reports a warning.
*/
const WARNING = 2;
/**
* Reports an information.
*/
const INFORMATION = 3;
/**
* Reports a hint.
*/
const HINT = 4;
}

View File

@ -1,25 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* A document highlight is a range inside a text document which deserves
* special attention. Usually a document highlight is visualized by changing
* the background color of its range.
*/
class DocumentHighlightKind
{
/**
* The range this highlight applies to.
*
* @var Range
*/
public $range;
/**
* The highlight kind, default is DocumentHighlightKind::TEXT.
*
* @var int|null
*/
public $kind;
}

View File

@ -1,24 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* A document highlight kind.
*/
abstract class DocumentHighlightKind
{
/**
* A textual occurrance.
*/
const TEXT = 1;
/**
* Read-access of a symbol, like reading a variable.
*/
const READ = 2;
/**
* Write-access of a symbol, like writing to a variable.
*/
const WRITE = 3;
}

View File

@ -1,23 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* Format document on type options
*/
class DocumentOnTypeFormattingOptions
{
/**
* A character on which formatting should be triggered, like `}`.
*
* @var string
*/
public $firstTriggerCharacter;
/**
* More trigger characters.
*
* @var string[]|null
*/
public $moreTriggerCharacter;
}

View File

@ -1,17 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* Enum
*/
abstract class ErrorCode
{
const PARSE_ERROR = -32700;
const INVALID_REQUEST = -32600;
const METHOD_NOT_FOUND = -32601;
const INVALID_PARAMS = -32602;
const INTERNAL_ERROR = -32603;
const SERVER_ERROR_START = -32099;
const SERVER_ERROR_END = -32000;
}

View File

@ -1,24 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* The file event type. Enum
*/
abstract class FileChangeType
{
/**
* The file got created.
*/
const CREATED = 1;
/**
* The file got changed.
*/
const CHANGED = 2;
/**
* The file got deleted.
*/
const DELETED = 3;
}

View File

@ -1,33 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* An event describing a file change.
*/
class FileEvent
{
/**
* The file's URI.
*
* @var string
*/
public $uri;
/**
* The change type.
*
* @var int
*/
public $type;
/**
* @param string $uri
* @param int $type
*/
public function __construct(string $uri, int $type)
{
$this->uri = $uri;
$this->type = $type;
}
}

View File

@ -1,25 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* Value-object describing what options formatting should use.
*/
class FormattingOptions
{
/**
* Size of a tab in spaces.
*
* @var int
*/
public $tabSize;
/**
* Prefer spaces over tabs.
*
* @var bool
*/
public $insertSpaces;
// Can be extended with further properties.
}

View File

@ -1,33 +0,0 @@
<?php
namespace LanguageServer\Protocol;
/**
* The result of a hover request.
*/
class Hover
{
/**
* The hover's content
*
* @var string|MarkedString|string[]|MarkedString[]
*/
public $contents;
/**
* An optional range
*
* @var Range|null
*/
public $range;
/**
* @param string|MarkedString|string[]|MarkedString[] $contents The hover's content
* @param Range $range An optional range
*/
public function __construct($contents = null, $range = null)
{
$this->contents = $contents;
$this->range = $range;
}
}

View File

@ -1,21 +0,0 @@
<?php
namespace LanguageServer\Protocol;
class InitializeResult
{
/**
* The capabilities the language server provides.
*
* @var LanguageServer\Protocol\ServerCapabilities
*/
public $capabilities;
/**
* @param LanguageServer\Protocol\ServerCapabilities $capabilities
*/
public function __construct(ServerCapabilities $capabilities = null)
{
$this->capabilities = $capabilities ?? new ServerCapabilities();
}
}

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