1
0
Fork 0

Compare commits

...

301 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
Talv 14a6d65832 Fix missing '()' for function definition (#340) 2017-04-02 00:30:10 +02:00
Jens Hausdorf 4d0a0a2a10 show anything from a doc comment block (#315) 2017-03-19 12:15:39 +01:00
Harings Rob 546660f957 Update README.md (#329)
Updating the used by to no longer link to the deprecated repository.
2017-03-08 16:19:56 +01:00
Sara Itani 7f8eccb5ae Fix formatting breakage (option 2) (#324)
PHP_CodeSniffer 3.0 RC4 introduces a breaking change that removes PHPCS from the composer autoloader. This fix addresses the issue by locking to v3.0 RC3.
2017-03-02 10:07:47 +01:00
Sara Itani 0de7ba8335 Ensure diagnostics are cleared on file deletion (#319)
* Ensure diagnostics are cleared on file deletion
Previously, error diagnostics would not be cleared when a file was deleted while it was closed. This would result in lingering errors in the problems view that could only be cleared by reloading the language server. This fix addresses the issue by adding support for workspace/didChangeWatchedFiles and automatically clearing diagnostics for deleted files.

* add FileEvent constructor
2017-03-01 11:18:37 +01:00
Matthew Brown 56bd465bf8 DefinitionResolver fixes (#307)
* Fix class references

* Fix return types
2017-02-20 10:28:49 +01:00
Matthew Brown cbfd70d398 Abort traversal in NodeAtPositionFinder (#305) 2017-02-18 00:28:10 +01:00
Cameron Eagans 5d2ab8f369 Add test for #211 (#270) 2017-02-15 17:25:06 +01:00
Felix Becker 3856f4f46a Update PHPParser 2017-02-15 11:16:16 +01:00
Trevor Bortins d5c54ac30f Read vendor directory from project's composer.json, if set. (#281) 2017-02-07 23:20:12 +01:00
Felix Becker 571b26a0c3 Use php_uname() instead of PHP_OS (#283) 2017-02-06 16:42:45 +01:00
Felix Becker 3c11cde9fb Include packages-dev (#282) 2017-02-06 16:35:16 +01:00
Felix Becker 5100d89617 Make resolveExpressionNodeToType() handle null (#277) 2017-02-04 12:52:04 +01:00
rox b90ede7fb3 Neovim is using php language server (#272) 2017-02-04 12:04:19 +01:00
Felix Becker bedd157636 Caching (#260) 2017-02-04 00:20:38 +01:00
Cameron Eagans 34d3d2030d Automatically index stubs on composer install (#269) 2017-02-02 18:36:48 +01:00
Cameron Eagans 7fbd68a61a Depend on phpcs RC2 (#268) 2017-02-02 02:06:16 +01:00
Felix Becker d8823bc7dc Use JetBrains PHPStorm stubs from packagist (#264)
* Use JetBrains PHPStorm stubs from packagist

* Fix path
2017-01-30 11:55:13 +01:00
Felix Becker 47b5b6709c Restart without XDebug if enabled (#259) 2017-01-26 02:08:40 +01:00
Felix Becker 96aa998486 Make Index an EventEmitter (#255) 2017-01-25 01:38:11 +01:00
Ivan Bozhanov 43a91b0d09 Handle hover for $this (#249) 2017-01-19 15:47:11 +01:00
Felix Becker d080c161a9 Don't crash if indexing fails 2017-01-13 12:06:58 -08:00
Felix Becker 106aa24b5d Implement global references protocol extension (#236) 2017-01-10 17:08:52 -08:00
Felix Becker 49245fd4d3 Allow overriding (#229)
* Add missing documentLoader property

* Make PhpDocumentLoader->contentRetriever public
2017-01-04 19:18:14 -08:00
Felix Becker 662143abad Fix wrong function call in DefinitionResolver 2016-12-24 16:35:20 +01:00
Kaloyan Raev de0dd32a67 Add used by Eclipse IDE (LSP4E-PHP) (#224) 2016-12-23 11:10:44 +01:00
Felix Becker b93d4f33cb Fix stubs location 2016-12-20 13:53:15 +01:00
Felix Becker 710d2a7ff7 Fix autoloading in ComposerScripts 2016-12-20 13:44:01 +01:00
Felix Becker 2005518dfe Support find-all-references for namespaces (#221) 2016-12-17 03:46:08 +01:00
Felix Becker 83618fee2e Avoid multiple references with function calls (#220) 2016-12-16 23:42:47 +01:00
Felix Becker 2242a35678 Reset definitions and references on content update (#219) 2016-12-16 23:33:55 +01:00
Felix Becker d03db024c1 Add Eclipse Che to "Used By" section 2016-12-16 13:00:11 +01:00
Felix Becker a4a13e6528 Add support for inherited members (#218)
in completion, definition, references, hover etc
2016-12-16 01:40:17 +01:00
Felix Becker cc9d5e987b Fix wrong class name 2016-12-15 18:01:44 +01:00
Felix Becker cd116a252b Update php-parser 2016-12-13 10:07:11 +01:00
Felix Becker 0b61951a9c Support hover for definitions
#201
2016-12-13 03:18:07 +01:00
Felix Becker 96ea8608d7 Support getting references from a reference
Closes #201
2016-12-13 02:53:01 +01:00
Felix Becker b8a113ddd0 Correct reference collection for New_ nodes
Closes #202
2016-12-13 02:40:51 +01:00
Felix Becker d90a88e625 Add missing property declaration 2016-12-13 02:13:57 +01:00
Felix Becker d7fc9e0425 Index twice to collect dynamic references (#206) 2016-12-13 02:11:29 +01:00
Felix Becker a7d77d844e Add Index classes and stubs (#214) 2016-12-13 01:51:02 +01:00
Felix Becker b9f9871156 Files finder (#209) 2016-12-08 02:33:48 +01:00
Felix Becker ebd1cc6133 Refactor content retrieval (#208) 2016-12-08 01:51:32 +01:00
Michal Niewrzal db6f4f7e5d Don't filter properties on typed prefix (#207) 2016-12-07 21:17:55 +01:00
Felix Becker 10fb3c92e0 Completion (#165)
* Add support for method/property completion

* Move completion fixtures into directory

* Add support for variable suggestions

Refactor logic into CompletionProvider class

* Allow getTypeFromNode() to take Variable nodes

* Use property and constant values

* Fix using @var tag for variables

* Improve completion

* classes
* variables with prefix filtering

* Make FQNs more distinct

* use -> for instance methods/properties
* use ::$ for static properties

* Add tests for static access

* Properly filter completion on empty property

* Fix existing tests

* Add support for static access without prefix

* Fix testFullyQualifiedClass

* Add missing fixtures

* Correct file number in init test

* Only insert backslash if not typed yet

* Completion for keywords and bug fixes

* Correct variable insertion

* Support completion for namespaces

* Use CompletionList

* Always set isIncomplete to true

* Update PHPCodeSniffer

* Remove unused method

* And the call

* Handle case where FQN could not be resolved
2016-11-30 22:23:51 +01:00
Felix Becker f56b14438b Shutdown when the socket is closed (#191) 2016-11-30 21:10:05 +01:00
Felix Becker 5077b1a87a Add Dockerfile (#185)
* Add Dockerfile

* Add .dockerignore

* Publish to docker hub on every release
2016-11-29 21:08:54 +01:00
Felix Becker 48e0167060 Support to run as TCP server & fork a child process for every connection (#183) 2016-11-29 19:32:17 +01:00
Michal Niewrzal ea92b224cd Symbols throws error for empty php file (#187)
Closes #186
2016-11-29 13:10:02 +01:00
Michal Niewrzal e8ab8aa2b8 Make processId optional for initialization (#178) 2016-11-23 18:38:57 +01:00
Felix Becker 429114ff97 Handle group use declarations in DocBlockParser (#166) 2016-11-19 13:04:13 +01:00
Felix Becker 5213940064 Don't encode spaces to + 2016-11-19 12:25:52 +01:00
Felix Becker 8f6ee8dd02 Handle null return from getClosestNode() 2016-11-19 12:03:43 +01:00
Felix Becker 00bc8537a6 Support compound types when resolving FQNs 2016-11-19 11:45:25 +01:00
Felix Becker fb84741d55 Add missing property 2016-11-19 06:37:07 +01:00
Felix Becker c2ae7cd022 FIx crashes when tag doesn't have a type 2016-11-19 06:36:57 +01:00
Felix Becker 33211c68ca Resolve expressions recursively (#155)
* Add Definition class
* Add recursive DefinitionResolver
* Cache hover
2016-11-18 15:22:24 +01:00
Felix Becker c19aedcef2 Document that vendor errors are ignored 2016-11-18 15:01:28 +01:00
Felix Becker e254e66878 Improve installation guide 2016-11-18 14:59:08 +01:00
Felix Becker 5a8d64c18c Note that XDebug impacts performance 2016-11-18 14:57:24 +01:00
Felix Becker 4b014154ac Document support for the files extension 2016-11-18 14:56:22 +01:00
Michal Niewrzal 50490d51ea Fix formatting makes LS non-responsive (#153) 2016-11-18 14:25:05 +01:00
Felix Becker 500ae5dc92 Use custom error handler instead of Symfony (#162) 2016-11-18 14:15:08 +01:00
Felix Becker 12df6a7dd6 Add size limit back (#161) 2016-11-18 13:24:26 +01:00
Felix Becker 32b01afa90 Index files serially again (#157) 2016-11-17 22:20:37 +01:00
Felix Becker 6056f39d01 Fix LanguageServerTest failure 2016-11-17 22:08:05 +01:00
Felix Becker 601c9ad997 Update PHPParser to 3.0.0beta2 (#151) 2016-11-16 16:58:32 +01:00
Felix Becker 642425dede Ignore errors from dependencies (#147) 2016-11-14 20:00:10 +01:00
Felix Becker 03bbf5f4ba Enable LS to operate without accessing the file system (#136)
This PR decouples the LS from direct file system access by implementing the proposals for workspace/files and textDocument/content under workspace/xfiles and textDocument/xcontent. The requests are only used when the client expressed support for them through ClientCapabilities, otherwise direct FS access is used.
This turns document content retrieval and recursive file search into async operations.
In turn, all server handlers can now operate async by returning a promise.
2016-11-14 10:25:44 +01:00
Felix Becker 25f300c157 Add test for indexing 2016-11-07 11:52:24 +01:00
Felix Becker 47472252a7 Encode spaces in paths to %20 instead of + (#140) 2016-11-07 10:24:49 +01:00
Felix Becker 9e65cd4cf0 Correct ProtocolStreamReaderTest 2016-11-06 15:59:32 +01:00
Felix Becker 04ef6c8adf Handle Client responses (#128) 2016-10-31 11:47:21 +01:00
Felix Becker ff0a35d833 Simplify ProtocolStreamWriterTest 2016-10-31 10:23:37 +01:00
Felix Becker bec24383d4 Ignore errors when setting process title (#123) 2016-10-27 09:49:04 +02:00
Felix Becker 28dc42b5c0 Add note about versioning 2016-10-27 00:11:33 +02:00
Felix Becker 92145c526e Update sabre/event to 5.0 2016-10-27 00:03:09 +02:00
Felix Becker 99224b73e4 Improve README (#120) 2016-10-27 00:00:49 +02:00
Felix Becker 18e58b4ce8 Update php-parser to latest commit (#119) 2016-10-26 22:25:24 +02:00
Felix Becker ed41df0062 Remove caching for now (#118) 2016-10-26 21:35:57 +02:00
Michal Niewrzal 867196babf Definition for instanceof class (#117) 2016-10-26 20:56:02 +02:00
Michal Niewrzal 8a354ba1af Definition for use function (#116) 2016-10-26 11:47:02 +02:00
Stephan Unverwerth 6806ba94e0 Async ProtocolStreamWriter (#112) 2016-10-25 23:50:36 +02:00
Felix Becker cd3bf18fe2 Revert "Handle closed input or output stream (#110)"
This reverts commit 83afa0c1b8.
2016-10-24 23:20:15 +02:00
ADmad 5ecab683eb Linting (#107)
* Update travis config.
* Add phpcs config file.
* Exclude rules
* Ignore failures in tests
* Automatic fixes
* Inline ParsingMode enum as class constants
* Loosen FormatTest because of excluded rule
2016-10-24 19:35:37 +02:00
Kaloyan Raev 83afa0c1b8 Handle closed input or output stream (#110) 2016-10-24 13:46:39 +02:00
Michal Niewrzal a19d225a7a Fix definition for method return type (#76) 2016-10-21 16:51:11 +02:00
Felix Becker b16674d394 Improve logging 2016-10-20 16:46:34 +02:00
Felix Becker b9222b0fd1 Switch to serialize() instead of JSON (#104) 2016-10-20 04:31:07 +02:00
Felix Becker 1e7260a2ea Cache index on disk (#82) 2016-10-20 03:48:30 +02:00
Felix Becker 8e36e59e9a Fix crash 2016-10-20 03:36:17 +02:00
Felix Becker 953a8023b7 Update AdvancedJsonRpc (#103) 2016-10-20 03:31:12 +02:00
Felix Becker f8733c741c Remove PhpParser workaround (#102) 2016-10-20 02:13:15 +02:00
Felix Becker 1e00275e02 Hold SymbolInformation table in memory (#101) 2016-10-20 02:08:23 +02:00
Felix Becker 9cbca1cd7f Revert "Use SymbolKind::FIELD for class fields (#78)"
This reverts commit 2980941fd1.
2016-10-20 01:53:07 +02:00
Felix Becker 96694996f7 Refactor FQN functions to own namespace (#100) 2016-10-20 01:00:20 +02:00
Felix Becker e993b9994a Remove unneeded argument to ReferencesCollector 2016-10-20 00:20:30 +02:00
Felix Becker e19670c141 Resolve self, static, parent (#99) 2016-10-20 00:18:36 +02:00
Felix Becker 6bd1b10e4d Resolve $this (#98) 2016-10-20 00:10:47 +02:00
Kaloyan Raev 5f984e2826 Fix undefined index error (#96) 2016-10-19 15:22:08 +02:00
Felix Becker 44445e3af4 Remove old definition/references after reparse (#88) 2016-10-19 13:33:43 +02:00
Felix Becker 7668a0c695 Catch parse error in indexing (#83) 2016-10-19 12:41:53 +02:00
Felix Becker 4db7ffd88c Hover (#50)
* Add hover support

* Use context in DocBlockParser

* Improve DocBlockParser error handling

* Improve hover output

* Add more tests
2016-10-19 12:31:32 +02:00
Felix Becker 2e03aa32f3 Fix class constant tests 2016-10-19 12:26:22 +02:00
Felix Becker 6b6ec8c105 Symbol test refactor (#92)
* Don't use json_decode in symbol tests

* Remove custom setUp()

* Use getDefinitionLocation()

* TextDocumentTestCase -> ServerTestCase

* Refactor Workspace\SymbolTest
2016-10-18 23:09:51 +02:00
Felix Becker cdcfaf7849 Refactor Definition\GlobalFallbackTest 2016-10-18 15:18:50 +02:00
Felix Becker 691a0bddfe Refactor tests for easier changes to fixtures (#87) 2016-10-18 10:48:16 +02:00
Michal Niewrzal cba4357856 Fix file name for CompletionItemKind (#85) 2016-10-17 10:17:18 +02:00
Felix Becker 3290ec31b2 Cache vendor dir on Travis 2016-10-14 09:08:40 +02:00
Felix Becker 1e6917ef55 Add size limit of 0.5MB for indexing (#80) 2016-10-14 09:06:56 +02:00
Felix Becker 2980941fd1 Use SymbolKind::FIELD for class fields (#78) 2016-10-14 09:02:42 +02:00
Felix Becker c479969758 Add support for definition of static class access (#72)
Getting the definition of TestClass in
TestClass::staticTestMethod();
echo TestClass::$staticTestProperty;
echo TestClass::TEST_CLASS_CONST;
2016-10-12 12:40:13 +02:00
Felix Becker 6fe01183b0 References (#52)
* Adds support for textDocument/references
* Adds tests for global definitions and global fallback
2016-10-12 01:45:15 +02:00
Felix Becker 66b5176a43 Allow %-encoded colon after drive letter in URI 2016-10-12 00:53:21 +02:00
Michal Niewrzal f81d03948b Update README with --memory-limit param (#70) 2016-10-11 21:14:49 +02:00
Michal Niewrzal 8f7f975408 Add command line parameter for memory limit (#68) 2016-10-11 16:50:55 +02:00
Michal Niewrzal c667f83371 Remove unused use statements (#67) 2016-10-11 16:23:09 +02:00
Felix Becker 23b127a986 Add Symfony ErrorHandler (#58) 2016-10-11 14:50:10 +02:00
Felix Becker 15e004fb9b Only hold AST for open files in memory (#63)
* Only hold content for open files in memory

* Add test for didClose

* Remove invalid URI formatting test

* Don't keep AST in memory

* Fix symbol search crash

* Change Project map to FQN => URI

Removes PhpDocument::load(), isLoaded(), unload()

* Add docblocks

* Rename some functions

* Extend documentation

* Correct docblock
2016-10-11 14:42:56 +02:00
Kaloyan Raev d41cde2039 Return empty array instead of null for empty definitions result (#64) 2016-10-11 10:26:46 +02:00
Kaloyan Raev 1f808c59e1 Fixes #59: Handle correctly negative endLine in PHP Parser errors (#62)
* Fixes #59: Handle correctly negative endLine in PHP Parser errors

* Clearer $startLine calculation

* Add missing test file

* Better calculation of endLine

* Remove trailing spaces
2016-10-11 10:15:20 +02:00
Michal Niewrzal e75c1592fc Use PHP_CodeSniffer as a formatter (#35)
Also adds uriTopath util function
2016-10-10 15:06:02 +02:00
Felix Becker 18ac760bc6 Set process title 2016-10-10 12:59:07 +02:00
Felix Becker 03e4e34a4e Don't %-decode the document URI 2016-10-09 19:09:28 +02:00
Felix Becker aff9edb630 Don't crash on array assignments 2016-10-09 19:06:46 +02:00
Felix Becker 1689e4d0dc Extend symbol search tests 2016-10-09 16:37:17 +02:00
Felix Becker a74bf90d77 Split WorkspaceTest 2016-10-09 16:37:17 +02:00
Felix Becker 7032f806d4 Merge pull request #49 from felixfbecker/definition
Go to definition support
2016-10-09 16:25:58 +02:00
Felix Becker 0387f28759 Rename NodeVisitors NS to NodeVisitor 2016-10-09 16:03:56 +02:00
Felix Becker 7f95b76cf8 Refactor DefinitionResolver
Move logic to PhpDocument::getDefininedFqn() for reusability
Fix DefinitionResolverTest
2016-10-09 15:58:39 +02:00
Felix Becker 7322a6c658 Add fromNode() factories and correct columns 2016-10-09 15:58:39 +02:00
Felix Becker 3a880934e5 Split up PhpDocument::getDefinitionByNode() 2016-10-09 15:58:39 +02:00
Felix Becker 6be53ad658 Use DefinitionCollector for symbol requests 2016-10-09 15:58:39 +02:00
Felix Becker d4757e0a24 Add textDocument/definition support 2016-10-09 15:58:39 +02:00
Felix Becker 827ab4c842 Add Position::compare() and Range::includes() 2016-10-09 15:51:42 +02:00
Felix Becker fbdf1aa414 Add ownerDocument attribute to nodes 2016-10-09 15:51:42 +02:00
Felix Becker 987308fc0a Refactor TextDocument tests into separate classes 2016-10-09 15:51:42 +02:00
Felix Becker 48c71e5bc1 Add method to find out node at position
Keep AST in memory
2016-10-09 15:51:42 +02:00
Felix Becker 4786fe173c Decorate all nodes with parent, sibling references 2016-10-08 15:18:31 +02:00
Felix Becker 658a27f5a5 Add more symbols to symbol test
* constants
* static properties
* static methods
2016-10-08 15:02:04 +02:00
Felix Becker 063c7f9ad2 Move NodeVisitors to own namespace 2016-10-08 15:01:58 +02:00
Felix Becker 6cb916e28d Improve inline documentation and code style 2016-10-08 13:45:10 +02:00
Michal Niewrzal c962f81924 Fix for failing tests (#44) 2016-10-02 20:19:38 +02:00
Stephan Unverwerth 0c758ec815 Fix issue #42 and add regression test (#43) 2016-09-30 15:13:54 +02:00
Felix Becker dfc80a5c66 Convert indentation to spaces in phpunit.xml.dist 2016-09-30 12:20:53 +02:00
Felix Becker 5bc228a8e0 Add Gitter badge 2016-09-30 12:09:33 +02:00
Felix Becker 6917f1c789 Add more declare(strict_types = 1) (#39) 2016-09-30 11:54:49 +02:00
Felix Becker 9fd9a02e19 Change fixtures naming convention 2016-09-30 11:38:46 +02:00
Stephan Unverwerth 501d26e1d4 Global symbol search (#31)
* Implemented workspace symbol search

* Fixed missing TextEdit using declaration

* Fixed generating uri when parsing next file.

* Cleaned up code. Fixed tests

* Fixed PHPDoc for LanguageServer::initialize()

* Moved utility functions to utils.php

* Added tests for pathToUri and findFilesRecursive

* Added command line argument for socket communication

* Fixed local variable detection and containerName generation in SymbolFinder

* Fixed formatting in ProtocolStreamReader

* Store text content in PHPDocument, removed stmts, regenerate on demand

* Fixed local variable detection and containerName generation in SymbolFinder.

* Added Tests for Project and Workspace

* Added test for didChange event

* Modified lexer error handling

* Removed file that shouldn't have been committed.

* Updated sabre/event dependency to 4.0.0

* Updated readme.md to show tcp option

* make input stream non-blocking

* Correct code style

* Use triple equals

* Revert change in SymbolFinder

* Optimize processFile() a bit

* Use MessageType enum instead of number literal

* Add missing space

* Fixed ProtocolStreamWriter for nonblocking connection.

* Suppress fwrite() notice when not all bytes could be written.

* Fix another code style issue

* Throw Exceotion instead of Error

* Added ProtocolStreamWriter test

* Correct workspace/symbol documentation

* Improve exception in ProtocolStreamWriter::write()
2016-09-30 11:30:08 +02:00
Michal Niewrzal bc2d6b9b59 Add phpunit configuration file (#37) 2016-09-29 16:09:23 +02:00
Felix Becker 41e9fb7e8a Update PHPParser to 3.0.0beta1 (#33) 2016-09-23 18:56:04 +02:00
Kaloyan Raev 817056270e Shift end position in SymbolInformation ranges (#28)
Fixes #27
2016-09-14 09:45:24 +02:00
Levan Gabeskiria 4d5052bebd Exclude variable symbols (#16) 2016-09-09 19:57:28 +02:00
Felix Becker 2d4ca8f99a Add PHP version badge 2016-09-09 10:01:34 +02:00
Felix Becker a20a86c9b9 Add .idea to .gitignore 2016-09-08 21:07:42 +02:00
Felix Becker 72d776d638 Fix whitespace errors 2016-09-08 21:07:20 +02:00
Felix Becker 7138088b4f Add contribution guides 2016-09-06 14:02:28 +02:00
Felix Becker aa6b729336 Remove unneeded files 2016-09-06 13:00:23 +02:00
Felix Becker 4fc2a6c2e4 Add coverage reporting (#14)
* Add coverage reporting

* Add badge

* Add codecov.yml
2016-09-06 12:55:05 +02:00
Michal Niewrzal 6169998b92 Support document formatting (#10) 2016-09-06 12:54:34 +02:00
Felix Becker d1b9b33741 Fix character offset in publishDiagnotic test 2016-09-04 12:50:40 +02:00
Felix Becker 4e88a17de3 Correct diagnostic behavior 2016-09-04 12:43:58 +02:00
Felix Becker 23f641f78b Fix type references 2016-09-04 12:27:56 +02:00
Felix Becker db28e22378 Publish errors as diagnostics, improve tests 2016-09-02 21:13:30 +02:00
Felix Becker 57604e61f1 Make documentSymbol work 🎉 2016-09-02 02:56:45 +02:00
Felix Becker b8b038d0b0 Add LICENSE 2016-08-30 18:49:06 +02:00
Felix Becker 6183243b18 Add Gemnasium badge 2016-08-25 18:14:08 +02:00
Felix Becker 284bde2e36 Add missing import 2016-08-25 17:03:29 +02:00
Felix Becker 21034df05b Make ProtocolStreamWriter::write() public 2016-08-25 17:01:29 +02:00
Felix Becker b7e051f2ce Add missing imports 2016-08-25 16:58:57 +02:00
Felix Becker 8bdbebf8c7 Fix autoloading 2016-08-25 16:51:34 +02:00
Felix Becker f1a53a93de Rename bin entry 2016-08-25 16:23:28 +02:00
349 changed files with 24304 additions and 1559 deletions

10
.dockerignore Normal file
View File

@ -0,0 +1,10 @@
.DS_Store
.vscode/
.idea/
.git/
tests/
fixtures/
coverage/
coverage.xml
images/
node_modules/

View File

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

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

6
.gitignore vendored
View File

@ -1,4 +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,10 +1,60 @@
language: php
php:
- '7.0'
- '7.0'
- '7.2'
git:
depth: 10
submodules: false
cache:
directories:
- $HOME/Library/Caches/Homebrew
- $HOME/.composer/cache
- $HOME/.npm
install:
- composer install
- composer install --prefer-dist --no-interaction
- pecl install ast-1.0.0
script:
- vendor/bin/phpunit --bootstrap vendor/autoload.php tests
- vendor/bin/phpcs -n
- vendor/bin/phan
- vendor/bin/phpunit --coverage-clover=coverage.xml --colors=always
- bash <(curl -s https://codecov.io/bash)
jobs:
include:
- stage: test
os: osx
osx_image: xcode9.1
language: generic
before_install:
# Fix ruby error https://github.com/Homebrew/brew/issues/3299
- brew update
- brew install php@7.1
- brew link --force --overwrite php@7.1
- pecl install xdebug-2.6.0
- php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
- php composer-setup.php
- ln -s "`pwd`/composer.phar" /usr/local/bin/composer
- stage: release
php: '7.0'
services:
- docker
install:
- nvm install 8
- nvm use 8
- npm install
script:
- ./node_modules/.bin/semantic-release
stages:
- test
- name: release
if: branch = master AND type = push AND fork = false
branches:
only:
- master

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

24
Dockerfile Normal file
View File

@ -0,0 +1,24 @@
# Running this container will start a language server that listens for TCP connections on port 2088
# Every connection will be run in a forked child process
FROM composer AS builder
COPY ./ /app
RUN composer install
FROM php:7-cli
LABEL maintainer="Felix Becker <felix.b@outlook.com>"
RUN docker-php-ext-configure pcntl --enable-pcntl
RUN docker-php-ext-install pcntl
COPY ./php.ini /usr/local/etc/php/conf.d/
COPY --from=builder /app /srv/phpls
WORKDIR /srv/phpls
EXPOSE 2088
CMD ["--tcp-server=0:2088"]
ENTRYPOINT ["php", "bin/php-language-server.php"]

15
LICENSE.txt Normal file
View File

@ -0,0 +1,15 @@
ISC License
Copyright (c) 2016, Felix Frederick Becker
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

224
README.md
View File

@ -1,7 +1,225 @@
# 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)
[![License](https://img.shields.io/packagist/l/felixfbecker/language-server.svg)](https://packagist.org/packages/felixfbecker/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)
[![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)
A pure PHP implementation of the [Language Server Protocol](https://github.com/Microsoft/language-server-protocol).
A pure PHP implementation of the open [Language Server Protocol](https://github.com/Microsoft/language-server-protocol).
Provides static code analysis for PHP for any IDE.
Uses the great [Tolerant PHP Parser](https://github.com/Microsoft/tolerant-php-parser),
[phpDocumentor's DocBlock reflection](https://github.com/phpDocumentor/ReflectionDocBlock)
and an [event loop](http://sabre.io/event/loop/) for concurrency.
**Table of Contents**
- [Features](#features)
- [Performance](#performance)
- [Versioning](#versioning)
- [Installation](#installation)
- [Running](#running)
- [Used by](#used-by)
- [Contributing](#contributing)
## Features
### [Completion](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#textDocument_completion)
![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)
### [Find References](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#find-references-request)
![Find References demo](images/references.png)
### [Hover](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#hover-request)
![Hover class demo](images/hoverClass.png)
![Hover parameter demo](images/hoverParam.png)
A hover request returns a declaration line (marked with language `php`) and the summary of the docblock.
For Parameters, it will return the `@param` tag.
### [Document Symbols](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#document-symbols-request)
![Document Symbols demo](images/documentSymbol.gif)
### [Workspace Symbols](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#workspace-symbols-request)
![Workspace Symbols demo](images/workspaceSymbol.gif)
The query is matched case-insensitively against the fully qualified name of the symbol.
Non-Standard: An empty query will return _all_ symbols found in the workspace.
### Error reporting through [Publish Diagnostics](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#publishdiagnostics-notification)
![Error reporting demo](images/publishDiagnostics.png)
PHP parse errors are reported as errors, parse errors of docblocks are reported as warnings.
Errors/Warnings from the `vendor` directory are ignored.
### Stubs for PHP built-ins
Completion, type resolval etc. will use the standard PHP library and common extensions.
### What is considered a definition?
Globally searchable definitions are:
- classes
- interfaces
- traits
- properties
- methods
- class constants
- constants with `const` keyword
Definitions resolved just-in-time when needed:
- variable assignments
- parameters
- closure `use` statements
Not supported yet:
- constants with `define()`
Namespaces are not considerd a declaration by design because they only make up a part of the fully qualified name
and don't map to one unique declaration.
### What is considered a reference?
Definitions/references/hover currently work for
- class instantiations
- static method calls
- class constant access
- static property access
- parameter type hints
- return type hints
- method calls, if the variable was assigned to a new object in the same scope
- property access, if the variable was assigned to a new object in the same scope
- variables
- parameters
- imported closure variables (`use`)
- `use` statements for classes, constants and functions
- class-like after `implements`/`extends`
- function calls
- constant access
- `instanceof` checks
- Reassigned variables
- Nested access/calls on return values, properties, array access
### Protocol Extensions
This language server implements the [files protocol extension](https://github.com/sourcegraph/language-server-protocol/blob/master/extension-files.md).
If the client expresses support through `ClientCapabilities.xfilesProvider` and `ClientCapabilities.xcontentProvider`,
the server will request files in the workspace and file contents through requests from the client and never access
the file system directly. This allows the server to operate in an isolated environment like a container,
on a remote workspace or any a different protocol than `file://`.
## Performance
Upon initialization, the server will recursively scan the project directory for PHP files, parse them and add all definitions
and references to an in-memory index.
The time this takes depends on the project size.
At the time of writing, this project contains 78 files + 1560 files in dependencies which take 97s to parse
and consume 76 MB on a Surface Pro 3.
The language server is fully operational while indexing and can respond to requests with the definitions already indexed.
Follow-up requests will be almost instant because the index is kept in memory.
Having XDebug enabled heavily impacts performance and can even crash the server if the `max_nesting_level` setting is too low.
## Versioning
This project follows [semver](http://semver.org/) for the protocol communication and command line parameters,
e.g. a major version increase of the LSP will result in a major version increase of the PHP LS.
New features like request implementations will result in a new minor version.
Everything else will be a patch release.
All classes are considered internal and are not subject to semver.
## Installation
The recommended installation method is through [Composer](https://getcomposer.org/).
Simply run
composer require felixfbecker/language-server
and you will get the latest stable release and all dependencies.
Running `composer update` will update the server to the latest non-breaking version.
After installing the language server and its dependencies,
you must parse the stubs for standard PHP symbols and save the index for fast initialization.
composer run-script --working-dir=vendor/felixfbecker/language-server parse-stubs
## Running
Start the language server with
php vendor/felixfbecker/language-server/bin/php-language-server.php
### Command line arguments
#### `--tcp=host:port` (optional)
Causes the server to use a tcp connection for communicating with the language client instead of using STDIN/STDOUT.
The server will try to connect to the specified address.
Strongly recommended on Windows because of blocking STDIO.
Example:
php bin/php-language-server.php --tcp=127.0.0.1:12345
#### `--tcp-server=host:port` (optional)
Causes the server to use a tcp connection for communicating with the language client instead of using STDIN/STDOUT.
The server will listen on the given address for a connection.
If PCNTL is available, will fork a child process for every connection.
If not, will only accept one connection and the connection cannot be reestablished once closed, spawn a new process instead.
Example:
php bin/php-language-server.php --tcp-server=127.0.0.1:12345
#### `--memory-limit=integer` (optional)
Sets memory limit for language server.
Equivalent to [memory-limit](http://php.net/manual/en/ini.core.php#ini.memory-limit) php.ini directive.
The default is 4GB (which is way more than needed).
Example:
php bin/php-language-server.php --memory-limit=256M
## Used by
- [VS Code PHP IntelliSense](https://github.com/felixfbecker/vscode-php-intellisense)
- [Eclipse Che](https://eclipse.org/che/)
- [Eclipse IDE (LSP4E-PHP)](https://github.com/eclipselabs/lsp4e-php)
- NeoVim: [LanguageServer-php-neovim](https://github.com/roxma/LanguageServer-php-neovim) with [LanguageClient neovim](https://github.com/autozimu/LanguageClient-neovim)
- Atom: [ide-php](https://github.com/atom/ide-php)
## Contributing
You need at least PHP 7.0 and Composer installed.
Clone the repository and run
composer install
to install dependencies.
Run the tests with
composer test
Lint with
composer lint
The project parses PHPStorm's PHP stubs to get support for PHP builtins. It re-parses them as needed after Composer processes, but after some code changes (such as ones involving the index or parsing) you may have to explicitly re-parse them:
composer run-script parse-stubs
To debug with xDebug ensure that you have this set as an environment variable
PHPLS_ALLOW_XDEBUG=1
This tells the Language Server to not restart without XDebug if it detects that XDebug is enabled (XDebug has a high performance impact).

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,10 +0,0 @@
<?php
use LanguageServer\LanguageServer;
use Sabre\Event\Loop;
require __DIR__ . '../vendor/autoload.php';
$server = new LanguageServer(new ProtocolStreamReader(STDIN), new ProtocolStreamWriter(STDOUT));
Loop\run();

110
bin/php-language-server.php Normal file
View File

@ -0,0 +1,110 @@
<?php
use LanguageServer\{LanguageServer, ProtocolStreamReader, ProtocolStreamWriter, StderrLogger};
use Sabre\Event\Loop;
use Composer\XdebugHandler\XdebugHandler;
$options = getopt('', ['tcp::', 'tcp-server::', 'memory-limit::']);
ini_set('memory_limit', $options['memory-limit'] ?? '4G');
foreach ([__DIR__ . '/../../../autoload.php', __DIR__ . '/../autoload.php', __DIR__ . '/../vendor/autoload.php'] as $file) {
if (file_exists($file)) {
require $file;
break;
}
}
// Convert all errors to ErrorExceptions
set_error_handler(function (int $severity, string $message, string $file, int $line) {
if (!(error_reporting() & $severity)) {
// This error code is not included in error_reporting (can also be caused by the @ operator)
return;
}
throw new \ErrorException($message, 0, $severity, $file, $line);
});
$logger = new StderrLogger();
// Only write uncaught exceptions to STDERR, not STDOUT
set_exception_handler(function (\Throwable $e) use ($logger) {
$logger->critical((string)$e);
});
@cli_set_process_title('PHP Language Server');
// If XDebug is enabled, restart without it
$xdebugHandler = new XdebugHandler('PHPLS');
$xdebugHandler->setLogger($logger);
$xdebugHandler->check();
unset($xdebugHandler);
if (!empty($options['tcp'])) {
// Connect to a TCP server
$address = $options['tcp'];
$socket = stream_socket_client('tcp://' . $address, $errno, $errstr);
if ($socket === false) {
$logger->critical("Could not connect to language client. Error $errno\n$errstr");
exit(1);
}
stream_set_blocking($socket, false);
$ls = new LanguageServer(
new ProtocolStreamReader($socket),
new ProtocolStreamWriter($socket)
);
Loop\run();
} else if (!empty($options['tcp-server'])) {
// Run a TCP Server
$address = $options['tcp-server'];
$tcpServer = stream_socket_server('tcp://' . $address, $errno, $errstr);
if ($tcpServer === false) {
$logger->critical("Could not listen on $address. Error $errno\n$errstr");
exit(1);
}
$logger->debug("Server listening on $address");
$pcntlAvailable = extension_loaded('pcntl');
if (!$pcntlAvailable) {
$logger->notice('PCNTL is not available. Only a single connection will be accepted');
}
while ($socket = stream_socket_accept($tcpServer, -1)) {
$logger->debug('Connection accepted');
stream_set_blocking($socket, false);
if ($pcntlAvailable) {
// If PCNTL is available, fork a child process for the connection
// An exit notification will only terminate the child process
$pid = pcntl_fork();
if ($pid === -1) {
$logger->critical('Could not fork');
exit(1);
} else if ($pid === 0) {
// Child process
$reader = new ProtocolStreamReader($socket);
$writer = new ProtocolStreamWriter($socket);
$reader->on('close', function () use ($logger) {
$logger->debug('Connection closed');
});
$ls = new LanguageServer($reader, $writer);
Loop\run();
// Just for safety
exit(0);
}
} else {
// If PCNTL is not available, we only accept one connection.
// An exit notification will terminate the server
$ls = new LanguageServer(
new ProtocolStreamReader($socket),
new ProtocolStreamWriter($socket)
);
Loop\run();
}
}
} else {
// Use STDIO
$logger->debug('Listening on STDIN');
stream_set_blocking(STDIN, false);
$ls = new LanguageServer(
new ProtocolStreamReader(STDIN),
new ProtocolStreamWriter(STDOUT)
);
Loop\run();
}

16
codecov.yml Normal file
View File

@ -0,0 +1,16 @@
coverage:
status:
project:
default:
target: auto
threshold: null
base: auto
comment:
layout: "header, diff, tree, changes"
behavior: default
require_changes: false # if true: only post the comment if coverage changes
branches: null
flags: null
paths: null

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

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,5 @@
<?php
$abc = 1;
$abc2 = 2;
echo $ab

View File

@ -0,0 +1,3 @@
<?php
TestClass::TE

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,9 @@
<?php
namespace Whatever;
use TestNamespace\{TestClass, TestInterface};
\TestC
class OtherClass {}

View File

View File

@ -0,0 +1 @@
<

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,3 @@
<?php
cl

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,5 @@
<?php
namespace SomeNamespace {}
SomeNa

View File

@ -0,0 +1,4 @@
<?php
$obj = new ChildClass;
$obj->

View File

@ -0,0 +1,4 @@
<?php
$obj = new TestClass;
$obj->t

View File

@ -0,0 +1,3 @@
<?php
TestClass::

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,3 @@
<?php
TestClass::st

View File

@ -0,0 +1,3 @@
<?php
TestClass::$st

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
<?php
namespace Whatever;
use TestNamespace\{TestClass, TestInterface};
TestC
class OtherClass {}

View File

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

View File

@ -0,0 +1,7 @@
<?php
namespace Whatever;
use TestNamespace\{TestClass, TestInterface};
$obj = new

View File

@ -0,0 +1,10 @@
<?php
/**
* @param string|null $param A parameter
*/
function test(string $param = null)
{
$var = 123;
$
}

View File

@ -0,0 +1,10 @@
<?php
/**
* @param string|null $param A parameter
*/
function test(string $param = null)
{
$var = 123;
$p
}

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

@ -0,0 +1,10 @@
<?php
namespace GlobalFallback;
// Should fall back to global_symbols.php
test_function();
echo TEST_CONST;
// Should not fall back
$obj = new TestClass();

View File

@ -0,0 +1,43 @@
<?php
$obj = new TestClass();
$obj->testMethod();
echo $obj->testProperty;
TestClass::staticTestMethod();
echo TestClass::$staticTestProperty;
echo TestClass::TEST_CLASS_CONST;
test_function();
$var = 123;
echo $var;
/**
* Aute duis elit reprehenderit tempor cillum proident anim laborum eu laboris reprehenderit ea incididunt.
*
* @param TestClass $param Adipisicing non non cillum sint incididunt cillum enim mollit.
* @return TestClass
*/
function whatever(TestClass $param): TestClass {
echo $param;
}
$fn = function() use ($var) {
echo $var;
};
echo TEST_CONST;
use function test_function;
if ($abc instanceof TestInterface) {
}
// Nested expression
$obj->testProperty->testMethod();
TestClass::$staticTestProperty[123]->testProperty;
$child = new ChildClass;
echo $child->testMethod();

119
fixtures/global_symbols.php Normal file
View File

@ -0,0 +1,119 @@
<?php
/**
* Esse commodo excepteur pariatur Lorem est aute incididunt reprehenderit.
*
* @var int
*/
const TEST_CONST = 123;
/**
* Pariatur ut laborum tempor voluptate consequat ea deserunt.
*
* Deserunt enim minim sunt sint ea nisi. Deserunt excepteur tempor id nostrud
* laboris commodo ad commodo velit mollit qui non officia id. Nulla duis veniam
* veniam officia deserunt et non dolore mollit ea quis eiusmod sit non. Occaecat
* consequat sunt culpa exercitation pariatur id reprehenderit nisi incididunt Lorem
* sint. Officia culpa pariatur laborum nostrud cupidatat consequat mollit.
*/
class TestClass implements TestInterface
{
/**
* Anim labore veniam consectetur laboris minim quis aute aute esse nulla ad.
*
* @var int
*/
const TEST_CLASS_CONST = 123;
/**
* Lorem excepteur officia sit anim velit veniam enim.
*
* @var TestClass[]
*/
public static $staticTestProperty;
/**
* Reprehenderit magna velit mollit ipsum do.
*
* @var TestClass
*/
public $testProperty;
/**
* Do magna consequat veniam minim proident eiusmod incididunt aute proident.
*/
public static function staticTestMethod()
{
echo self::TEST_CLASS_CONST;
}
/**
* Non culpa nostrud mollit esse sunt laboris in irure ullamco cupidatat amet.
*
* @param TestClass $testParameter Lorem sunt velit incididunt mollit
* @return TestClass
*/
public function testMethod($testParameter): TestInterface
{
$this->testProperty = $testParameter;
}
}
trait TestTrait
{
}
interface TestInterface
{
}
/**
* Officia aliquip adipisicing et nulla et laboris dolore labore.
*
* @return void
*/
function test_function()
{
}
new class {
const TEST_CLASS_CONST = 123;
public static $staticTestProperty;
public $testProperty;
public static function staticTestMethod()
{
}
public function testMethod($testParameter)
{
$testVariable = 123;
}
};
class ChildClass extends TestClass {}
/**
* Lorem ipsum dolor sit amet, consectetur.
*/
define('TEST_DEFINE_CONSTANT', false);
print TEST_DEFINE_CONSTANT ? 'true' : 'false';
/**
* Neither this class nor its members are referenced anywhere
*/
class UnusedClass
{
public $unusedProperty;
public function unusedMethod()
{
}
}

View File

@ -0,0 +1,6 @@
<?php
interface class
{
}

View File

@ -0,0 +1,5 @@
<?php
echo "Hello";
namespace A;

43
fixtures/references.php Normal file
View File

@ -0,0 +1,43 @@
<?php
namespace TestNamespace;
$obj = new TestClass($a, $b, $c);
$obj->testMethod();
echo $obj->testProperty;
TestClass::staticTestMethod();
echo TestClass::$staticTestProperty;
echo TestClass::TEST_CLASS_CONST;
test_function();
$var = 123;
echo $var;
/**
* Aute duis elit reprehenderit tempor cillum proident anim laborum eu laboris reprehenderit ea incididunt.
*
* @param TestClass $param Adipisicing non non cillum sint incididunt cillum enim mollit.
* @return TestClass
*/
function whatever(TestClass $param): TestClass {
echo $param;
}
$fn = function() use ($var) {
echo $var;
};
echo TEST_CONST;
use function TestNamespace\test_function;
if ($abc instanceof TestInterface) {
}
// Nested expressions
$obj->testProperty->testMethod();
TestClass::$staticTestProperty[123]->testProperty;
$child = new ChildClass;
echo $child->testMethod();

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();

110
fixtures/symbols.php Normal file
View File

@ -0,0 +1,110 @@
<?php
namespace TestNamespace;
/**
* Esse commodo excepteur pariatur Lorem est aute incididunt reprehenderit.
*
* @var int
*/
const TEST_CONST = 123;
/**
* Pariatur ut laborum tempor voluptate consequat ea deserunt.
*
* Deserunt enim minim sunt sint ea nisi. Deserunt excepteur tempor id nostrud
* laboris commodo ad commodo velit mollit qui non officia id. Nulla duis veniam
* veniam officia deserunt et non dolore mollit ea quis eiusmod sit non. Occaecat
* consequat sunt culpa exercitation pariatur id reprehenderit nisi incididunt Lorem
* sint. Officia culpa pariatur laborum nostrud cupidatat consequat mollit.
*/
class TestClass implements TestInterface
{
/**
* Anim labore veniam consectetur laboris minim quis aute aute esse nulla ad.
*
* @var int
*/
const TEST_CLASS_CONST = 123;
/**
* Lorem excepteur officia sit anim velit veniam enim.
*
* @var TestClass[]
*/
public static $staticTestProperty;
/**
* Reprehenderit magna velit mollit ipsum do.
*
* @var TestClass
*/
public $testProperty;
/**
* Do magna consequat veniam minim proident eiusmod incididunt aute proident.
*/
public static function staticTestMethod()
{
echo self::TEST_CLASS_CONST;
}
/**
* Non culpa nostrud mollit esse sunt laboris in irure ullamco cupidatat amet.
*
* @param TestClass $testParameter Lorem sunt velit incididunt mollit
* @return TestClass
*/
public function testMethod($testParameter): TestInterface
{
$this->testProperty = $testParameter;
}
}
trait TestTrait
{
}
interface TestInterface
{
}
/**
* Officia aliquip adipisicing et nulla et laboris dolore labore.
*
* @return void
*/
function test_function()
{
}
new class {
const TEST_CLASS_CONST = 123;
public static $staticTestProperty;
public $testProperty;
public static function staticTestMethod()
{
}
public function testMethod($testParameter)
{
$testVariable = 123;
}
};
class ChildClass extends TestClass {}
class Example {
public function __construct() {}
public function __destruct() {}
}
namespace TestNamespace\InnerNamespace;
class InnerClass {
}

6
fixtures/use.php Normal file
View File

@ -0,0 +1,6 @@
<?php
namespace SecondTestNamespace;
use TestNamespace\TestClass;
use TestNamespace\{TestTrait, TestInterface};

BIN
images/completion.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

BIN
images/definition.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

BIN
images/documentSymbol.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

BIN
images/hoverClass.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
images/hoverParam.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
images/references.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

BIN
images/signatureHelp.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

BIN
images/workspaceSymbol.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 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"
}
]
}
}

5
php.ini Normal file
View File

@ -0,0 +1,5 @@
# php.ini for Docker
error_reporting = E_ALL
display_errors = stderr

13
phpcs.xml.dist Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0"?>
<ruleset name="PHP Language Server">
<file>src</file>
<file>tests</file>
<exclude-pattern>tests/Validation/cases</exclude-pattern>
<rule ref="PSR2">
<exclude name="PSR2.Namespaces.UseDeclaration.MultipleDeclarations"/>
<exclude name="PSR2.ControlStructures.ElseIfDeclaration.NotAllowed"/>
<exclude name="PSR2.ControlStructures.ControlStructureSpacing.SpacingAfterOpenBrace"/>
<exclude name="Squiz.WhiteSpace.ControlStructureSpacing.SpacingBeforeClose"/>
<exclude name="Squiz.WhiteSpace.ControlStructureSpacing.SpacingAfterOpen"/>
</rule>
</ruleset>

30
phpunit.xml.dist Normal file
View File

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

29
src/Cache/Cache.php Normal file
View File

@ -0,0 +1,29 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Cache;
use Sabre\Event\Promise;
/**
* A key/value store for caching purposes
*/
interface Cache
{
/**
* Gets a value from the cache
*
* @param string $key
* @return Promise <mixed>
*/
public function get(string $key): Promise;
/**
* Sets a value in the cache
*
* @param string $key
* @param mixed $value
* @return Promise
*/
public function set(string $key, $value): Promise;
}

53
src/Cache/ClientCache.php Normal file
View File

@ -0,0 +1,53 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Cache;
use LanguageServer\LanguageClient;
use Sabre\Event\Promise;
/**
* Caches content through a xcache/* requests
*/
class ClientCache implements Cache
{
/**
* @var LanguageClient
*/
public $client;
/**
* @param LanguageClient $client
*/
public function __construct(LanguageClient $client)
{
$this->client = $client;
}
/**
* Gets a value from the cache
*
* @param string $key
* @return Promise <mixed>
*/
public function get(string $key): Promise
{
return $this->client->xcache->get($key)->then('unserialize')->otherwise(function () {
// Ignore
});
}
/**
* Sets a value in the cache
*
* @param string $key
* @param mixed $value
* @return Promise
*/
public function set(string $key, $value): Promise
{
return $this->client->xcache->set($key, serialize($value))->otherwise(function () {
// Ignore
});
}
}

View File

@ -0,0 +1,67 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Cache;
use Sabre\Event\Promise;
/**
* Caches content on the file system
*/
class FileSystemCache implements Cache
{
/**
* @var string
*/
public $cacheDir;
public function __construct()
{
if (strtoupper(substr(php_uname('s'), 0, 3)) === 'WIN') {
$this->cacheDir = getenv('LOCALAPPDATA') . '\\PHP Language Server\\';
} else if (getenv('XDG_CACHE_HOME')) {
$this->cacheDir = getenv('XDG_CACHE_HOME') . '/phpls/';
} else {
$this->cacheDir = getenv('HOME') . '/.phpls/';
}
}
/**
* Gets a value from the cache
*
* @param string $key
* @return Promise <mixed>
*/
public function get(string $key): Promise
{
try {
$file = $this->cacheDir . urlencode($key);
if (!file_exists($file)) {
return Promise\resolve(null);
}
return Promise\resolve(unserialize(file_get_contents($file)));
} catch (\Exception $e) {
return Promise\resolve(null);
}
}
/**
* Sets a value in the cache
*
* @param string $key
* @param mixed $value
* @return Promise
*/
public function set(string $key, $value): Promise
{
try {
$file = $this->cacheDir . urlencode($key);
if (!file_exists($this->cacheDir)) {
mkdir($this->cacheDir);
}
file_put_contents($file, serialize($value));
} finally {
return Promise\resolve(null);
}
}
}

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Client;
use LanguageServer\ClientHandler;
use LanguageServerProtocol\{Diagnostic, TextDocumentItem, TextDocumentIdentifier};
use Sabre\Event\Promise;
use JsonMapper;
/**
* Provides method handlers for all textDocument/* methods
*/
class TextDocument
{
/**
* @var ClientHandler
*/
private $handler;
/**
* @var JsonMapper
*/
private $mapper;
public function __construct(ClientHandler $handler, JsonMapper $mapper)
{
$this->handler = $handler;
$this->mapper = $mapper;
}
/**
* Diagnostics notification are sent from the server to the client to signal results of validation runs.
*
* @param string $uri
* @param Diagnostic[] $diagnostics
* @return Promise <void>
*/
public function publishDiagnostics(string $uri, array $diagnostics): Promise
{
return $this->handler->notify('textDocument/publishDiagnostics', [
'uri' => $uri,
'diagnostics' => $diagnostics
]);
}
/**
* The content request is sent from a server to a client
* to request the current content of a text document identified by the URI
*
* @param TextDocumentIdentifier $textDocument The document to get the content for
* @return Promise <TextDocumentItem> The document's current content
*/
public function xcontent(TextDocumentIdentifier $textDocument): Promise
{
return $this->handler->request(
'textDocument/xcontent',
['textDocument' => $textDocument]
)->then(function ($result) {
return $this->mapper->map($result, new TextDocumentItem);
});
}
}

48
src/Client/Window.php Normal file
View File

@ -0,0 +1,48 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Client;
use LanguageServer\ClientHandler;
use Sabre\Event\Promise;
/**
* Provides method handlers for all window/* methods
*/
class Window
{
/**
* @var ClientHandler
*/
private $handler;
public function __construct(ClientHandler $handler)
{
$this->handler = $handler;
}
/**
* The show message notification is sent from a server to a client
* to ask the client to display a particular message in the user interface.
*
* @param int $type
* @param string $message
* @return Promise <void>
*/
public function showMessage(int $type, string $message): Promise
{
return $this->handler->notify('window/showMessage', ['type' => $type, 'message' => $message]);
}
/**
* The log message notification is sent from the server to the client to ask the client to log a particular message.
*
* @param int $type
* @param string $message
* @return Promise <void>
*/
public function logMessage(int $type, string $message): Promise
{
return $this->handler->notify('window/logMessage', ['type' => $type, 'message' => $message]);
}
}

47
src/Client/Workspace.php Normal file
View File

@ -0,0 +1,47 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Client;
use LanguageServer\ClientHandler;
use LanguageServerProtocol\TextDocumentIdentifier;
use Sabre\Event\Promise;
use JsonMapper;
/**
* Provides method handlers for all workspace/* methods
*/
class Workspace
{
/**
* @var ClientHandler
*/
private $handler;
/**
* @var JsonMapper
*/
private $mapper;
public function __construct(ClientHandler $handler, JsonMapper $mapper)
{
$this->handler = $handler;
$this->mapper = $mapper;
}
/**
* Returns a list of all files in a directory
*
* @param string $base The base directory (defaults to the workspace)
* @return Promise <TextDocumentIdentifier[]> Array of documents
*/
public function xfiles(string $base = null): Promise
{
return $this->handler->request(
'workspace/xfiles',
['base' => $base]
)->then(function (array $textDocuments) {
return $this->mapper->mapArray($textDocuments, [], TextDocumentIdentifier::class);
});
}
}

42
src/Client/XCache.php Normal file
View File

@ -0,0 +1,42 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Client;
use LanguageServer\ClientHandler;
use Sabre\Event\Promise;
/**
* Provides method handlers for all xcache/* methods
*/
class XCache
{
/**
* @var ClientHandler
*/
private $handler;
public function __construct(ClientHandler $handler)
{
$this->handler = $handler;
}
/**
* @param string $key
* @return Promise <mixed>
*/
public function get(string $key): Promise
{
return $this->handler->request('xcache/get', ['key' => $key]);
}
/**
* @param string $key
* @param mixed $value
* @return Promise <mixed>
*/
public function set(string $key, $value): Promise
{
return $this->handler->notify('xcache/set', ['key' => $key, 'value' => $value]);
}
}

80
src/ClientHandler.php Normal file
View File

@ -0,0 +1,80 @@
<?php
declare(strict_types = 1);
namespace LanguageServer;
use AdvancedJsonRpc;
use Sabre\Event\Promise;
class ClientHandler
{
/**
* @var ProtocolReader
*/
public $protocolReader;
/**
* @var ProtocolWriter
*/
public $protocolWriter;
/**
* @var IdGenerator
*/
public $idGenerator;
public function __construct(ProtocolReader $protocolReader, ProtocolWriter $protocolWriter)
{
$this->protocolReader = $protocolReader;
$this->protocolWriter = $protocolWriter;
$this->idGenerator = new IdGenerator;
}
/**
* Sends a request to the client and returns a promise that is resolved with the result or rejected with the error
*
* @param string $method The method to call
* @param array|object $params The method parameters
* @return Promise <mixed> Resolved with the result of the request or rejected with an error
*/
public function request(string $method, $params): Promise
{
$id = $this->idGenerator->generate();
return $this->protocolWriter->write(
new Message(
new AdvancedJsonRpc\Request($id, $method, (object)$params)
)
)->then(function () use ($id) {
$promise = new Promise;
$listener = function (Message $msg) use ($id, $promise, &$listener) {
if (AdvancedJsonRpc\Response::isResponse($msg->body) && $msg->body->id === $id) {
// Received a response
$this->protocolReader->removeListener('message', $listener);
if (AdvancedJsonRpc\SuccessResponse::isSuccessResponse($msg->body)) {
$promise->fulfill($msg->body->result);
} else {
$promise->reject($msg->body->error);
}
}
};
$this->protocolReader->on('message', $listener);
return $promise;
});
}
/**
* Sends a notification to the client
*
* @param string $method The method to call
* @param array|object $params The method parameters
* @return Promise <null> Will be resolved as soon as the notification has been sent
*/
public function notify(string $method, $params): Promise
{
return $this->protocolWriter->write(
new Message(
new AdvancedJsonRpc\Notification($method, (object)$params)
)
);
}
}

View File

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

707
src/CompletionProvider.php Normal file
View File

@ -0,0 +1,707 @@
<?php
declare(strict_types = 1);
namespace LanguageServer;
use LanguageServer\Index\ReadableIndex;
use LanguageServer\Factory\CompletionItemFactory;
use LanguageServerProtocol\{
TextEdit,
Range,
Position,
CompletionList,
CompletionItem,
CompletionItemKind,
CompletionContext,
CompletionTriggerKind
};
use Microsoft\PhpParser;
use Microsoft\PhpParser\Node;
use Microsoft\PhpParser\ResolvedName;
use Generator;
use function LanguageServer\FqnUtilities\{
nameConcat,
nameGetFirstPart,
nameGetParent,
nameStartsWith,
nameWithoutFirstPart
};
class CompletionProvider
{
const KEYWORDS = [
'?>',
'__halt_compiler',
'abstract',
'and',
'array',
'as',
'break',
'callable',
'case',
'catch',
'class',
'clone',
'const',
'continue',
'declare',
'default',
'die',
'do',
'echo',
'else',
'elseif',
'empty',
'enddeclare',
'endfor',
'endforeach',
'endif',
'endswitch',
'endwhile',
'eval',
'exit',
'extends',
'false',
'final',
'finally',
'for',
'foreach',
'function',
'global',
'goto',
'if',
'implements',
'include',
'include_once',
'instanceof',
'insteadof',
'interface',
'isset',
'list',
'namespace',
'new',
'null',
'or',
'print',
'private',
'protected',
'public',
'require',
'require_once',
'return',
'static',
'switch',
'throw',
'trait',
'true',
'try',
'unset',
'use',
'var',
'while',
'xor',
'yield',
// List of other reserved words (http://php.net/manual/en/reserved.other-reserved-words.php)
// (the ones which do not occur as actual keywords above.)
'int',
'float',
'bool',
'string',
'void',
'iterable',
'object',
// Pseudo keywords
'from', // As in yield from
'strict_types',
'ticks', // As in declare(ticks=1)
'encoding', // As in declare(encoding='EBCDIC')
];
/**
* @var DefinitionResolver
*/
private $definitionResolver;
/**
* @var Project
*/
private $project;
/**
* @var ReadableIndex
*/
private $index;
/**
* @param DefinitionResolver $definitionResolver
* @param ReadableIndex $index
*/
public function __construct(DefinitionResolver $definitionResolver, ReadableIndex $index)
{
$this->definitionResolver = $definitionResolver;
$this->index = $index;
}
/**
* Returns suggestions for a specific cursor position in a document
*
* @param PhpDocument $doc The opened document
* @param Position $pos The cursor position
* @param CompletionContext $context The completion context
* @return CompletionList
*/
public function provideCompletion(
PhpDocument $doc,
Position $pos,
CompletionContext $context = null
): CompletionList {
// This can be made much more performant if the tree follows specific invariants.
$node = $doc->getNodeAtPosition($pos);
// Get the node at the position under the cursor
$offset = $node === null ? -1 : $pos->toOffset($node->getFileContents());
if (
$node !== null
&& $offset > $node->getEndPosition()
&& $node->parent !== null
&& $node->parent->getLastChild() instanceof PhpParser\MissingToken
) {
$node = $node->parent;
}
$list = new CompletionList;
$list->isIncomplete = true;
if ($node instanceof Node\Expression\Variable &&
$node->parent instanceof Node\Expression\ObjectCreationExpression &&
$node->name instanceof PhpParser\MissingToken
) {
$node = $node->parent;
}
// Inspect the type of expression under the cursor
$content = $doc->getContent();
$offset = $pos->toOffset($content);
if (
$node === null
|| (
$node instanceof Node\Statement\InlineHtml
&& (
$context !== null
// Make sure to not suggest on the > trigger character in HTML
&& (
$context->triggerKind === CompletionTriggerKind::INVOKED
|| $context->triggerCharacter === '<'
)
)
)
|| $pos == new Position(0, 0)
) {
// HTML, beginning of file
// Inside HTML and at the beginning of the file, propose <?php
$item = new CompletionItem('<?php', CompletionItemKind::KEYWORD);
$item->textEdit = new TextEdit(
new Range($pos, $pos),
stripStringOverlap($doc->getRange(new Range(new Position(0, 0), $pos)), '<?php')
);
$list->items[] = $item;
} elseif (
$node instanceof Node\Expression\Variable
&& !(
$node->parent instanceof Node\Expression\ScopedPropertyAccessExpression
&& $node->parent->memberName === $node
)
) {
// Variables
//
// $|
// $a|
// Find variables, parameters and use statements in the scope
$namePrefix = $node->getName() ?? '';
foreach ($this->suggestVariablesAtNode($node, $namePrefix) as $var) {
$item = new CompletionItem;
$item->kind = CompletionItemKind::VARIABLE;
$item->label = '$' . $var->getName();
$item->documentation = $this->definitionResolver->getDocumentationFromNode($var);
$item->detail = (string)$this->definitionResolver->getTypeFromNode($var);
$item->textEdit = new TextEdit(
new Range($pos, $pos),
stripStringOverlap($doc->getRange(new Range(new Position(0, 0), $pos)), $item->label)
);
$list->items[] = $item;
}
} elseif ($node instanceof Node\Expression\MemberAccessExpression) {
// Member access expressions
//
// $a->c|
// $a->|
// Multiple prefixes for all possible types
$fqns = FqnUtilities\getFqnsFromType(
$this->definitionResolver->resolveExpressionNodeToType($node->dereferencableExpression)
);
// The FQNs of the symbol and its parents (eg the implemented interfaces)
foreach ($this->expandParentFqns($fqns) as $parentFqn) {
// Add the object access operator to only get members of all parents
$prefix = $parentFqn . '->';
$prefixLen = strlen($prefix);
// Collect fqn definitions
foreach ($this->index->getChildDefinitionsForFqn($parentFqn) as $fqn => $def) {
if (substr($fqn, 0, $prefixLen) === $prefix && $def->isMember) {
$list->items[] = CompletionItemFactory::fromDefinition($def);
}
}
}
} elseif (
($scoped = $node->parent) instanceof Node\Expression\ScopedPropertyAccessExpression ||
($scoped = $node) instanceof Node\Expression\ScopedPropertyAccessExpression
) {
// Static class members and constants
//
// A\B\C::$a|
// A\B\C::|
// A\B\C::$|
// A\B\C::foo|
//
// TODO: $a::|
// Resolve all possible types to FQNs
$fqns = FqnUtilities\getFqnsFromType(
$classType = $this->definitionResolver->resolveExpressionNodeToType($scoped->scopeResolutionQualifier)
);
// The FQNs of the symbol and its parents (eg the implemented interfaces)
foreach ($this->expandParentFqns($fqns) as $parentFqn) {
// Append :: operator to only get static members of all parents
$prefix = strtolower($parentFqn . '::');
$prefixLen = strlen($prefix);
// Collect fqn definitions
foreach ($this->index->getChildDefinitionsForFqn($parentFqn) as $fqn => $def) {
if (substr(strtolower($fqn), 0, $prefixLen) === $prefix && $def->isMember) {
$list->items[] = CompletionItemFactory::fromDefinition($def);
}
}
}
} elseif (
ParserHelpers\isConstantFetch($node)
// Creation gets set in case of an instantiation (`new` expression)
|| ($creation = $node->parent) instanceof Node\Expression\ObjectCreationExpression
|| (($creation = $node) instanceof Node\Expression\ObjectCreationExpression)
) {
// Class instantiations, function calls, constant fetches, class names
//
// new MyCl|
// my_func|
// MY_CONS|
// MyCla|
// \MyCla|
// The name Node under the cursor
$nameNode = isset($creation) ? $creation->classTypeDesignator : $node;
if ($nameNode instanceof Node\QualifiedName) {
/** @var string The typed name. */
$prefix = (string)PhpParser\ResolvedName::buildName($nameNode->nameParts, $nameNode->getFileContents());
} else {
$prefix = $nameNode->getText($node->getFileContents());
}
$namespaceNode = $node->getNamespaceDefinition();
/** @var string The current namespace without a leading backslash. */
$currentNamespace = $namespaceNode === null ? '' : $namespaceNode->name->getText();
/** @var bool Whether the prefix is qualified (contains at least one backslash) */
$isFullyQualified = false;
/** @var bool Whether the prefix is qualified (contains at least one backslash) */
$isQualified = false;
if ($nameNode instanceof Node\QualifiedName) {
$isFullyQualified = $nameNode->isFullyQualifiedName();
$isQualified = $nameNode->isQualifiedName();
}
/** @var bool Whether we are in a new expression */
$isCreation = isset($creation);
/** @var array Import (use) tables */
$importTables = $node->getImportTablesForCurrentScope();
if ($isFullyQualified) {
// \Prefix\Goes\Here| - Only return completions from the root namespace.
/** @var $items \Generator|CompletionItem[] Generator yielding CompletionItems indexed by their FQN */
$items = $this->getCompletionsForFqnPrefix($prefix, $isCreation, false);
} else if ($isQualified) {
// Prefix\Goes\Here|
$items = $this->getPartiallyQualifiedCompletions(
$prefix,
$currentNamespace,
$importTables,
$isCreation
);
} else {
// PrefixGoesHere|
$items = $this->getUnqualifiedCompletions($prefix, $currentNamespace, $importTables, $isCreation);
}
$list->items = array_values(iterator_to_array($items));
foreach ($list->items as $item) {
// Remove ()
if (is_string($item->insertText) && substr($item->insertText, strlen($item->insertText) - 2) === '()') {
$item->insertText = substr($item->insertText, 0, -2);
}
}
}
return $list;
}
private function getPartiallyQualifiedCompletions(
string $prefix,
string $currentNamespace,
array $importTables,
bool $requireCanBeInstantiated
): \Generator {
// If the first part of the partially qualified name matches a namespace alias,
// only definitions below that alias can be completed.
list($namespaceAliases,,) = $importTables;
$prefixFirstPart = nameGetFirstPart($prefix);
$foundAlias = $foundAliasFqn = null;
foreach ($namespaceAliases as $alias => $aliasFqn) {
if (strcasecmp($prefixFirstPart, $alias) === 0) {
$foundAlias = $alias;
$foundAliasFqn = (string)$aliasFqn;
break;
}
}
if ($foundAlias !== null) {
yield from $this->getCompletionsFromAliasedNamespace(
$prefix,
$foundAlias,
$foundAliasFqn,
$requireCanBeInstantiated
);
} else {
yield from $this->getCompletionsForFqnPrefix(
nameConcat($currentNamespace, $prefix),
$requireCanBeInstantiated,
false
);
}
}
/**
* Yields completions for non-qualified global names.
*
* Yields
* - Aliased classes
* - Completions from current namespace
* - Roamed completions from the global namespace (when not creating and not already in root NS)
* - PHP keywords (when not creating)
*
* @return \Generator|CompletionItem[]
* Yields CompletionItems
*/
private function getUnqualifiedCompletions(
string $prefix,
string $currentNamespace,
array $importTables,
bool $requireCanBeInstantiated
): \Generator {
// Aliases
list($namespaceAliases,,) = $importTables;
// use Foo\Bar
yield from $this->getCompletionsForAliases(
$prefix,
$namespaceAliases,
$requireCanBeInstantiated
);
// Completions from the current namespace
yield from $this->getCompletionsForFqnPrefix(
nameConcat($currentNamespace, $prefix),
$requireCanBeInstantiated,
false
);
if ($currentNamespace !== '' && $prefix === '') {
// Get additional suggestions from the global namespace.
// When completing e.g. for new |, suggest \DateTime
yield from $this->getCompletionsForFqnPrefix('', $requireCanBeInstantiated, true);
}
if (!$requireCanBeInstantiated) {
if ($currentNamespace !== '' && $prefix !== '') {
// Roamed definitions (i.e. global constants and functions). The prefix is checked against '', since
// in that case global completions have already been provided (including non-roamed definitions.)
yield from $this->getRoamedCompletions($prefix);
}
// Lastly and least importantly, suggest keywords.
yield from $this->getCompletionsForKeywords($prefix);
}
}
/**
* Gets completions for prefixes of fully qualified names in their parent namespace.
*
* @param string $prefix Prefix to complete for. Fully qualified.
* @param bool $requireCanBeInstantiated If set, only return classes.
* @param bool $insertFullyQualified If set, return completion with the leading \ inserted.
* @return \Generator|CompletionItem[]
* Yields CompletionItems.
*/
private function getCompletionsForFqnPrefix(
string $prefix,
bool $requireCanBeInstantiated,
bool $insertFullyQualified
): \Generator {
$namespace = nameGetParent($prefix);
foreach ($this->index->getChildDefinitionsForFqn($namespace) as $fqn => $def) {
if ($requireCanBeInstantiated && !$def->canBeInstantiated) {
continue;
}
if (!nameStartsWith($fqn, $prefix)) {
continue;
}
$completion = CompletionItemFactory::fromDefinition($def);
if ($insertFullyQualified) {
$completion->insertText = '\\' . $fqn;
}
yield $fqn => $completion;
}
}
/**
* Gets completions for non-qualified names matching the start of an used class, function, or constant.
*
* @param string $prefix Non-qualified name being completed for
* @param QualifiedName[] $aliases Array of alias FQNs indexed by the alias.
* @return \Generator|CompletionItem[]
* Yields CompletionItems.
*/
private function getCompletionsForAliases(
string $prefix,
array $aliases,
bool $requireCanBeInstantiated
): \Generator {
foreach ($aliases as $alias => $aliasFqn) {
if (!nameStartsWith($alias, $prefix)) {
continue;
}
$definition = $this->index->getDefinition((string)$aliasFqn);
if ($definition) {
if ($requireCanBeInstantiated && !$definition->canBeInstantiated) {
continue;
}
$completionItem = CompletionItemFactory::fromDefinition($definition);
$completionItem->insertText = $alias;
yield (string)$aliasFqn => $completionItem;
}
}
}
/**
* Gets completions for partially qualified names, where the first part is matched by an alias.
*
* @return \Generator|CompletionItem[]
* Yields CompletionItems.
*/
private function getCompletionsFromAliasedNamespace(
string $prefix,
string $alias,
string $aliasFqn,
bool $requireCanBeInstantiated
): \Generator {
$prefixFirstPart = nameGetFirstPart($prefix);
// Matched alias.
$resolvedPrefix = nameConcat($aliasFqn, nameWithoutFirstPart($prefix));
$completionItems = $this->getCompletionsForFqnPrefix(
$resolvedPrefix,
$requireCanBeInstantiated,
false
);
// Convert FQNs in the CompletionItems so they are expressed in terms of the alias.
foreach ($completionItems as $fqn => $completionItem) {
/** @var string $fqn with the leading parts determined by the alias removed. Has the leading backslash. */
$nameWithoutAliasedPart = substr($fqn, strlen($aliasFqn));
$completionItem->insertText = $alias . $nameWithoutAliasedPart;
yield $fqn => $completionItem;
}
}
/**
* Gets completions for globally defined functions and constants (i.e. symbols which may be used anywhere)
*
* @return \Generator|CompletionItem[]
* Yields CompletionItems.
*/
private function getRoamedCompletions(string $prefix): \Generator
{
foreach ($this->index->getChildDefinitionsForFqn('') as $fqn => $def) {
if (!$def->roamed || !nameStartsWith($fqn, $prefix)) {
continue;
}
$completionItem = CompletionItemFactory::fromDefinition($def);
// Second-guessing the user here - do not trust roaming to work. If the same symbol is
// inserted in the current namespace, the code will stop working.
$completionItem->insertText = '\\' . $fqn;
yield $fqn => $completionItem;
}
}
/**
* Completes PHP keywords.
*
* @return \Generator|CompletionItem[]
* Yields CompletionItems.
*/
private function getCompletionsForKeywords(string $prefix): \Generator
{
foreach (self::KEYWORDS as $keyword) {
if (nameStartsWith($keyword, $prefix)) {
$item = new CompletionItem($keyword, CompletionItemKind::KEYWORD);
$item->insertText = $keyword;
yield $keyword => $item;
}
}
}
/**
* Yields FQNs from an array along with the FQNs of all parent classes
*
* @param string[] $fqns
* @return Generator
*/
private function expandParentFqns(array $fqns) : Generator
{
foreach ($fqns as $fqn) {
yield $fqn;
$def = $this->index->getDefinition($fqn);
if ($def !== null) {
foreach ($def->getAncestorDefinitions($this->index) as $name => $def) {
yield $name;
}
}
}
}
/**
* Will walk the AST upwards until a function-like node is met
* and at each level walk all previous siblings and their children to search for definitions
* of that variable
*
* @param Node $node
* @param string $namePrefix Prefix to filter
* @return array <Node\Expr\Variable|Node\Param|Node\Expr\ClosureUse>
*/
private function suggestVariablesAtNode(Node $node, string $namePrefix = ''): array
{
$vars = [];
// Find variables in the node itself
// When getting completion in the middle of a function, $node will be the function node
// so we need to search it
foreach ($this->findVariableDefinitionsInNode($node, $namePrefix) as $var) {
// Only use the first definition
if (!isset($vars[$var->name])) {
$vars[$var->name] = $var;
}
}
// Walk the AST upwards until a scope boundary is met
$level = $node;
while ($level && !($level instanceof PhpParser\FunctionLike)) {
// Walk siblings before the node
$sibling = $level;
while ($sibling = $sibling->getPreviousSibling()) {
// Collect all variables inside the sibling node
foreach ($this->findVariableDefinitionsInNode($sibling, $namePrefix) as $var) {
$vars[$var->getName()] = $var;
}
}
$level = $level->parent;
}
// If the traversal ended because a function was met,
// also add its parameters and closure uses to the result list
if ($level && $level instanceof PhpParser\FunctionLike && $level->parameters !== null) {
foreach ($level->parameters->getValues() as $param) {
$paramName = $param->getName();
if (empty($namePrefix) || strpos($paramName, $namePrefix) !== false) {
$vars[$paramName] = $param;
}
}
if ($level instanceof Node\Expression\AnonymousFunctionCreationExpression
&& $level->anonymousFunctionUseClause !== null
&& $level->anonymousFunctionUseClause->useVariableNameList !== null) {
foreach ($level->anonymousFunctionUseClause->useVariableNameList->getValues() as $use) {
$useName = $use->getName();
if (empty($namePrefix) || strpos($useName, $namePrefix) !== false) {
$vars[$useName] = $use;
}
}
}
}
return array_values($vars);
}
/**
* Searches the subnodes of a node for variable assignments
*
* @param Node $node
* @param string $namePrefix Prefix to filter
* @return Node\Expression\Variable[]
*/
private function findVariableDefinitionsInNode(Node $node, string $namePrefix = ''): array
{
$vars = [];
// If the child node is a variable assignment, save it
$isAssignmentToVariable = function ($node) {
return $node instanceof Node\Expression\AssignmentExpression;
};
if ($this->isAssignmentToVariableWithPrefix($node, $namePrefix)) {
$vars[] = $node->leftOperand;
} elseif ($node instanceof Node\ForeachKey || $node instanceof Node\ForeachValue) {
foreach ($node->getDescendantNodes() as $descendantNode) {
if ($descendantNode instanceof Node\Expression\Variable
&& ($namePrefix === '' || strpos($descendantNode->getName(), $namePrefix) !== false)
) {
$vars[] = $descendantNode;
}
}
} else {
// Get all descendent variables, then filter to ones that start with $namePrefix.
// Avoiding closure usage in tight loop
foreach ($node->getDescendantNodes($isAssignmentToVariable) as $descendantNode) {
if ($this->isAssignmentToVariableWithPrefix($descendantNode, $namePrefix)) {
$vars[] = $descendantNode->leftOperand;
}
}
}
return $vars;
}
private function isAssignmentToVariableWithPrefix(Node $node, string $namePrefix): bool
{
return $node instanceof Node\Expression\AssignmentExpression
&& $node->leftOperand instanceof Node\Expression\Variable
&& ($namePrefix === '' || strpos($node->leftOperand->getName(), $namePrefix) !== false);
}
}

72
src/ComposerScripts.php Normal file
View File

@ -0,0 +1,72 @@
<?php
declare(strict_types = 1);
namespace LanguageServer;
use LanguageServer\FilesFinder\FileSystemFilesFinder;
use LanguageServer\ContentRetriever\FileSystemContentRetriever;
use LanguageServer\Index\StubsIndex;
use phpDocumentor\Reflection\DocBlockFactory;
use Webmozart\PathUtil\Path;
use Sabre\Uri;
use function Sabre\Event\coroutine;
use Microsoft\PhpParser;
foreach ([__DIR__ . '/../../../autoload.php', __DIR__ . '/../autoload.php', __DIR__ . '/../vendor/autoload.php'] as $file) {
if (file_exists($file)) {
require $file;
break;
}
}
class ComposerScripts
{
public static function parseStubs()
{
coroutine(function () {
$index = new StubsIndex;
$finder = new FileSystemFilesFinder;
$contentRetriever = new FileSystemContentRetriever;
$docBlockFactory = DocBlockFactory::createInstance();
$parser = new PhpParser\Parser();
$definitionResolver = new DefinitionResolver($index);
$stubsLocation = null;
foreach ([__DIR__ . '/../../../jetbrains/phpstorm-stubs', __DIR__ . '/../vendor/jetbrains/phpstorm-stubs'] as $dir) {
if (file_exists($dir)) {
$stubsLocation = Path::canonicalize($dir);
break;
}
}
if (!$stubsLocation) {
throw new \Exception('jetbrains/phpstorm-stubs package not found');
}
$uris = yield $finder->find("$stubsLocation/**/*.php");
foreach ($uris as $uri) {
echo "Parsing $uri\n";
$content = yield $contentRetriever->retrieve($uri);
// Change URI to phpstubs://
$parts = Uri\parse($uri);
$parts['path'] = Path::makeRelative($parts['path'], $stubsLocation);
$parts['scheme'] = 'phpstubs';
$uri = Uri\build($parts);
// Create a new document and add it to $index
new PhpDocument($uri, $content, $index, $parser, $docBlockFactory, $definitionResolver);
}
$index->setComplete();
echo "Saving Index\n";
$index->save();
echo "Finished\n";
})->wait();
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\ContentRetriever;
use LanguageServer\LanguageClient;
use LanguageServerProtocol\{TextDocumentIdentifier, TextDocumentItem};
use Sabre\Event\Promise;
/**
* Retrieves file content from the client through a textDocument/xcontent request
*/
class ClientContentRetriever implements ContentRetriever
{
/**
* @param LanguageClient $client
*/
public function __construct(LanguageClient $client)
{
$this->client = $client;
}
/**
* Retrieves the content of a text document identified by the URI through a textDocument/xcontent request
*
* @param string $uri The URI of the document
* @return Promise <string> Resolved with the content as a string
*/
public function retrieve(string $uri): Promise
{
return $this->client->textDocument->xcontent(new TextDocumentIdentifier($uri))
->then(function (TextDocumentItem $textDocument) {
return $textDocument->text;
});
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\ContentRetriever;
use Sabre\Event\Promise;
/**
* Interface for retrieving the content of a text document
*/
interface ContentRetriever
{
/**
* Retrieves the content of a text document identified by the URI
*
* @param string $uri The URI of the document
* @return Promise <string> Resolved with the content as a string
*/
public function retrieve(string $uri): Promise;
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\ContentRetriever;
use Sabre\Event\Promise;
use function LanguageServer\uriToPath;
/**
* Retrieves document content from the file system
*/
class FileSystemContentRetriever implements ContentRetriever
{
/**
* Retrieves the content of a text document identified by the URI from the file system
*
* @param string $uri The URI of the document
* @return Promise <string> Resolved with the content as a string
*/
public function retrieve(string $uri): Promise
{
return Promise\resolve(file_get_contents(uriToPath($uri)));
}
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types = 1);
namespace LanguageServer;
/**
* Thrown when the document content is not parsed because it exceeds the size limit
*/
class ContentTooLargeException extends \Exception
{
/**
* The URI of the file that exceeded the limit
*
* @var string
*/
public $uri;
/**
* The size of the file in bytes
*
* @var int
*/
public $size;
/**
* The limit that was exceeded in bytes
*
* @var int
*/
public $limit;
/**
* @param string $uri The URI of the file that exceeded the limit
* @param int $size The size of the file in bytes
* @param int $limit The limit that was exceeded in bytes
* @param \Throwable $previous The previous exception used for the exception chaining.
*/
public function __construct(string $uri, int $size, int $limit, \Throwable $previous = null)
{
$this->uri = $uri;
$this->size = $size;
$this->limit = $limit;
parent::__construct("$uri exceeds size limit of $limit bytes ($size)", 0, $previous);
}
}

136
src/Definition.php Normal file
View File

@ -0,0 +1,136 @@
<?php
declare(strict_types = 1);
namespace LanguageServer;
use LanguageServer\Index\ReadableIndex;
use phpDocumentor\Reflection\{Types, Type, TypeResolver};
use LanguageServerProtocol\SymbolInformation;
use Generator;
/**
* Class used to represent symbols
*/
class Definition
{
/**
* The fully qualified name of the symbol, if it has one
*
* Examples of FQNs:
* - testFunction()
* - TestNamespace
* - TestNamespace\TestClass
* - TestNamespace\TestClass::TEST_CONSTANT
* - TestNamespace\TestClass::$staticTestProperty
* - TestNamespace\TestClass->testProperty
* - TestNamespace\TestClass::staticTestMethod()
* - TestNamespace\TestClass->testMethod()
*
* @var string|null
*/
public $fqn;
/**
* For class or interfaces, the FQNs of extended classes and implemented interfaces
*
* @var string[]
*/
public $extends;
/**
* False for classes, interfaces, traits, functions and non-class constants
* True for methods, properties and class constants
* This is so methods and properties are not suggested in the global scope
*
* @var bool
*/
public $isMember;
/**
* True if this definition is affected by global namespace fallback (global function or global constant)
*
* @var bool
*/
public $roamed;
/**
* False for instance methods and properties
*
* @var bool
*/
public $isStatic;
/**
* True if the Definition is a class
*
* @var bool
*/
public $canBeInstantiated;
/**
* @var SymbolInformation
*/
public $symbolInformation;
/**
* The type a reference to this symbol will resolve to.
* For properties and constants, this is the type of the property/constant.
* For functions and methods, this is the return type.
* For any other declaration it will be null.
* Can also be a compound type.
* If it is unknown, will be Types\Mixed_.
*
* @var Type|null
*/
public $type;
/**
* The first line of the declaration, for use in textDocument/hover
*
* @var string
*/
public $declarationLine;
/**
* A documentation string, for use in textDocument/hover
*
* @var string
*/
public $documentation;
/**
* Signature information if this definition is for a FunctionLike, for use in textDocument/signatureHelp
*
* @var SignatureInformation
*/
public $signatureInformation;
/**
* Yields the definitons of all ancestor classes (the Definition fqn is yielded as key)
*
* @param ReadableIndex $index the index to search for needed definitions
* @param bool $includeSelf should the first yielded value be the current definition itself
* @return Generator
*/
public function getAncestorDefinitions(ReadableIndex $index, bool $includeSelf = false): Generator
{
if ($includeSelf) {
yield $this->fqn => $this;
}
if ($this->extends !== null) {
// iterating once, storing the references and iterating again
// guarantees that closest definitions are yielded first
$definitions = [];
foreach ($this->extends as $fqn) {
$def = $index->getDefinition($fqn);
if ($def !== null) {
yield $def->fqn => $def;
$definitions[] = $def;
}
}
foreach ($definitions as $def) {
yield from $def->getAncestorDefinitions($index);
}
}
}
}

1363
src/DefinitionResolver.php Normal file

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

@ -0,0 +1,49 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\FilesFinder;
use LanguageServer\LanguageClient;
use Sabre\Event\Promise;
use Sabre\Uri;
use Webmozart\Glob\Glob;
/**
* Retrieves file content from the client through a textDocument/xcontent request
*/
class ClientFilesFinder implements FilesFinder
{
/**
* @var LanguageClient
*/
private $client;
/**
* @param LanguageClient $client
*/
public function __construct(LanguageClient $client)
{
$this->client = $client;
}
/**
* Returns all files in the workspace that match a glob.
* If the client does not support workspace/files, it falls back to searching the file system directly.
*
* @param string $glob
* @return Promise <string[]> The URIs
*/
public function find(string $glob): Promise
{
return $this->client->workspace->xfiles()->then(function (array $textDocuments) use ($glob) {
$uris = [];
foreach ($textDocuments as $textDocument) {
$path = Uri\parse($textDocument->uri)['path'];
if (Glob::match($path, $glob)) {
$uris[] = $textDocument->uri;
}
}
return $uris;
});
}
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\FilesFinder;
use Sabre\Event\Promise;
use function Sabre\Event\coroutine;
use function LanguageServer\{pathToUri, timeout};
class FileSystemFilesFinder implements FilesFinder
{
/**
* Returns all files in the workspace that match a glob.
* If the client does not support workspace/xfiles, it falls back to searching the file system directly.
*
* @param string $glob
* @return Promise <string[]>
*/
public function find(string $glob): Promise
{
return coroutine(function () use ($glob) {
$uris = [];
foreach (new GlobIterator($glob) as $path) {
// Exclude any directories that also match the glob pattern
if (!is_dir($path) || !is_readable($path)) {
$uris[] = pathToUri($path);
}
yield timeout();
}
return $uris;
});
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\FilesFinder;
use Sabre\Event\Promise;
/**
* Interface for finding files in the workspace
*/
interface FilesFinder
{
/**
* Returns all files in the workspace that match a glob.
* If the client does not support workspace/xfiles, it falls back to searching the file system directly.
*
* @param string $glob
* @return Promise <string[]>
*/
public function find(string $glob): Promise;
}

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

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

25
src/IdGenerator.php Normal file
View File

@ -0,0 +1,25 @@
<?php
declare(strict_types = 1);
namespace LanguageServer;
/**
* Generates unique, incremental IDs for use as request IDs
*/
class IdGenerator
{
/**
* @var int
*/
public $counter = 1;
/**
* Returns a unique ID
*
* @return int
*/
public function generate()
{
return $this->counter++;
}
}

View File

@ -0,0 +1,155 @@
<?php
declare(strict_types = 1);
namespace LanguageServer\Index;
use LanguageServer\Definition;
use Sabre\Event\EmitterTrait;
abstract class AbstractAggregateIndex implements ReadableIndex
{
use EmitterTrait;
/**
* Returns all indexes managed by the aggregate index
*
* @return ReadableIndex[]
*/
abstract protected function getIndexes(): array;
public function __construct()
{
foreach ($this->getIndexes() as $index) {
$this->registerIndex($index);
}
}
/**
* @param ReadableIndex $index
*/
protected function registerIndex(ReadableIndex $index)
{
$index->on('complete', function () {
if ($this->isComplete()) {
$this->emit('complete');
}
});
$index->on('static-complete', function () {
if ($this->isStaticComplete()) {
$this->emit('static-complete');
}
});
$index->on('definition-added', function () {
$this->emit('definition-added');
});
}
/**
* Marks this index as complete
*
* @return void
*/
public function setComplete()
{
foreach ($this->getIndexes() as $index) {
$index->setComplete();
}
}
/**
* Marks this index as complete for static definitions and references
*
* @return void
*/
public function setStaticComplete()
{
foreach ($this->getIndexes() as $index) {
$index->setStaticComplete();
}
}
/**
* Returns true if this index is complete
*
* @return bool
*/
public function isComplete(): bool
{
foreach ($this->getIndexes() as $index) {
if (!$index->isComplete()) {
return false;
}
}
return true;
}
/**
* Returns true if this index is complete for static definitions or references
*
* @return bool
*/
public function isStaticComplete(): bool
{
foreach ($this->getIndexes() as $index) {
if (!$index->isStaticComplete()) {
return false;
}
}
return true;
}
/**
* Returns a Generator providing an associative array [string => Definition]
* that maps fully qualified symbol names to Definitions (global or not)
*
* @return \Generator yields Definition
*/
public function getDefinitions(): \Generator
{
foreach ($this->getIndexes() as $index) {
yield from $index->getDefinitions();
}
}
/**
* Returns a Generator that yields all the direct child Definitions of a given FQN
*
* @param string $fqn
* @return \Generator yields Definition
*/
public function getChildDefinitionsForFqn(string $fqn): \Generator
{
foreach ($this->getIndexes() as $index) {
yield from $index->getChildDefinitionsForFqn($fqn);
}
}
/**
* Returns the Definition object by a specific FQN
*
* @param string $fqn
* @param bool $globalFallback Whether to fallback to global if the namespaced FQN was not found
* @return Definition|null
*/
public function getDefinition(string $fqn, bool $globalFallback = false)
{
foreach ($this->getIndexes() as $index) {
if ($def = $index->getDefinition($fqn, $globalFallback)) {
return $def;
}
}
}
/**
* Returns a Generator providing all URIs in this index that reference a symbol
*
* @param string $fqn The fully qualified name of the symbol
* @return \Generator yields string
*/
public function getReferenceUris(string $fqn): \Generator
{
foreach ($this->getIndexes() as $index) {
yield from $index->getReferenceUris($fqn);
}
}
}

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