Add @param information in signature help
[hiphop-php.git] / hphp / hack / test / integration / test_lsp.py
blob6be1cf379032177dabafd364ff313035c0f5a9d3
1 # pyre-strict
3 from __future__ import absolute_import, division, print_function, unicode_literals
5 import copy
6 import json
7 import os
8 import re
9 import unittest
10 import urllib.parse
11 from typing import Iterable, List, Mapping, Tuple
13 import common_tests
14 from hh_paths import hh_server
15 from lspcommand import LspCommandProcessor, Transcript
16 from lsptestspec import LspTestSpec
17 from test_case import TestCase
18 from utils import Json, JsonObject, interpolate_variables
21 class LspTestDriver(common_tests.CommonTestDriver):
22 def write_load_config(self, use_saved_state: bool = False) -> None:
23 # Will use the .hhconfig already in the repo directory
24 # As for hh.conf, we'll write it explicitly each test.
25 # Note that hh.conf uses lower-case...
26 use_saved_state_str = "true" if use_saved_state else "false"
27 with open(os.path.join(self.repo_dir, "hh.conf"), "w") as f:
28 f.write(
29 """
30 use_watchman = true
31 watchman_subscribe_v2 = true
32 interrupt_on_watchman = true
33 interrupt_on_client = true
34 max_workers = 2
35 load_state_natively_v4 = {use_saved_state}
36 use_mini_state = {use_saved_state}
37 require_mini_state = {use_saved_state}
38 lazy_decl = {use_saved_state}
39 lazy_parse = {use_saved_state}
40 lazy_init2 = {use_saved_state}
41 """.format(
42 use_saved_state=use_saved_state_str
46 def write_naming_table_saved_state(self) -> str:
47 naming_table_saved_state_path = os.path.join(
48 self.repo_dir, "naming_table_saved_state.sqlite"
50 (stdout, stderr, retcode) = self.proc_call(
52 hh_server,
53 "--check",
54 self.repo_dir,
55 "--save-naming",
56 naming_table_saved_state_path,
59 assert retcode == 0, (
60 f"Failed to save naming table saved state: {retcode}\n"
61 + f"STDOUT:\n{stdout}\n"
62 + f"STDERR:\n{stderr}\n"
64 return naming_table_saved_state_path
67 class TestLsp(TestCase[LspTestDriver]):
68 @classmethod
69 def get_test_driver(cls) -> LspTestDriver:
70 return LspTestDriver()
72 @classmethod
73 def get_template_repo(cls) -> str:
74 return "hphp/hack/test/integration/data/lsp_exchanges/"
76 def repo_file(self, file: str) -> str:
77 return os.path.join(self.test_driver.repo_dir, file)
79 def read_repo_file(self, file: str) -> str:
80 with open(self.repo_file(file), "r") as f:
81 return f.read()
83 def repo_file_uri(self, file: str) -> str:
84 return urllib.parse.urljoin("file://", self.repo_file(file))
86 def parse_test_data(self, file: str, variables: Mapping[str, str]) -> Json:
87 text = self.read_repo_file(file)
88 data: Json = json.loads(text)
89 data = interpolate_variables(data, variables)
90 return data
92 def load_test_data(
93 self, test_name: str, variables: Mapping[str, str]
94 ) -> Tuple[Json, Json]:
95 test = self.parse_test_data(test_name + ".json", variables)
96 expected = self.parse_test_data(test_name + ".expected", variables)
97 return (test, expected)
99 def write_observed(self, test_name: str, observed_transcript: Json) -> None:
100 file = os.path.join(self.test_driver.template_repo, test_name + ".observed.log")
101 text = json.dumps(
102 list(self.get_important_received_items(observed_transcript)), indent=2
104 with open(file, "w") as f:
105 f.write(text)
107 def order_response(self, response: JsonObject) -> str:
108 if "id" in response:
109 return str(response["id"])
110 else:
111 return json.dumps(response, indent=2)
113 # sorts a list of responses using the 'id' parameter so they can be
114 # compared in sequence even if they came back from the server out of sequence.
115 # this can happen based on how json rpc is specified to work.
116 # if 'id' isn't present the response is a notification. we sort notifications
117 # by their entire text.
118 def sort_responses(self, responses: Iterable[JsonObject]) -> List[JsonObject]:
119 return sorted(responses, key=lambda response: self.order_response(response))
121 # removes stack traces from error responses since these can be noisy
122 # as code changes and they contain execution environment specific details
123 # by ignoring these when comparing responses we might miss some minor issues
124 # but will still catch the core error being thrown or not.
125 def sanitize_exceptions(
126 self, responses: Iterable[JsonObject]
127 ) -> Iterable[JsonObject]:
128 sanitized = copy.deepcopy(responses)
129 for response in sanitized:
130 if "error" in response:
131 if "data" in response["error"]:
132 if "stack" in response["error"]["data"]:
133 del response["error"]["data"]["stack"]
134 return sanitized
136 # dumps an LSP response into a standard json format that can be used for
137 # doing precise text comparison in a way that is human readable in the case
138 # of there being an error.
139 def serialize_responses(self, responses: Iterable[Json]) -> List[str]:
140 return [json.dumps(response, indent=2) for response in responses]
142 # generates received responses from an LSP communication transcript
143 # ignoring the non-deterministic ones "progress" and "actionRequired"
144 def get_important_received_items(self, transcript: Transcript) -> Iterable[Json]:
145 for entry in transcript.values():
146 received = entry.received or None
147 if received is None:
148 continue
149 method = received.get("method") or ""
150 if method in [
151 "window/progress",
152 "window/actionRequired",
153 "window/showStatus",
154 "telemetry/event",
156 continue
157 yield received
159 # gets a set of loaded responses ready for validation by sorting them
160 # by id and serializing them for precise text comparison
161 def prepare_responses(self, responses: Iterable[JsonObject]) -> List[str]:
162 return self.serialize_responses(
163 self.sanitize_exceptions(self.sort_responses(responses))
166 def run_lsp_test(
167 self,
168 test_name: str,
169 test: Json,
170 expected: Json,
171 wait_for_server: bool,
172 use_serverless_ide: bool,
173 ) -> None:
174 if wait_for_server:
175 assert not use_serverless_ide, (
176 "Warning: both `wait_for_server` and `use_serverless_ide` "
177 + "were set to `True` for testing in "
178 + self.run_lsp_test.__name__
179 + ". "
180 + "While this is a possible test case, it hasn't been written yet, "
181 + "so it's more likely that this is a mistake "
182 + "and you're accidentally relying on hh_server to fulfill "
183 + "serverless IDE requests."
184 + "(If you're writing that test, "
185 + "then it's time to remove this assertion.)"
188 # wait until hh_server is ready before starting lsp
189 self.test_driver.run_check()
190 elif use_serverless_ide:
191 self.test_driver.stop_hh_server()
193 with LspCommandProcessor.create(
194 self.test_driver.test_env, use_serverless_ide=use_serverless_ide
195 ) as lsp:
196 observed_transcript = lsp.communicate(test)
198 self.write_observed(test_name, observed_transcript)
200 expected_items = self.prepare_responses(expected)
201 observed_items = self.prepare_responses(
202 list(self.get_important_received_items(observed_transcript))
205 if not use_serverless_ide:
206 # If the server's busy, maybe the machine's just under too much
207 # pressure to give results in a timely fashion. Doing a retry would
208 # only defer the question of what to do in that case, so instead
209 # we'll just skip.
210 self.throw_on_skip(observed_transcript)
212 # validation checks that the number of items matches and that
213 # the responses are exactly identical to what we expect
214 self.assertEqual(
215 len(expected_items),
216 len(observed_items),
217 "Wrong count. Observed this:\n"
218 + json.dumps(observed_transcript, indent=2, separators=(",", ": ")),
220 for i in range(len(expected_items)):
221 self.assertEqual(expected_items[i], observed_items[i])
223 def throw_on_skip(self, transcript: Transcript) -> None:
224 failure_messages = ["Server busy", "timed out"]
225 for entry in transcript.values():
226 received = entry.received
227 if received is None:
228 continue
229 if received.get("error"):
230 message = received["error"]["message"]
231 for failure_message in failure_messages:
232 if failure_message in message:
233 raise unittest.SkipTest(message)
235 def prepare_server_environment(self) -> None:
236 self.maxDiff = None
237 self.test_driver.write_load_config()
238 self.test_driver.start_hh_server()
239 (output, err, _) = self.test_driver.run_check()
240 if "Error: Ran out of retries" in err:
241 raise unittest.SkipTest("Hack server could not be launched")
242 self.assertEqual(output.strip(), "No errors!")
244 def prepare_serverless_ide_environment(self) -> Mapping[str, str]:
245 self.maxDiff = None
246 self.test_driver.write_load_config(use_saved_state=False)
247 naming_table_saved_state_path = (
248 self.test_driver.write_naming_table_saved_state()
250 return {"naming_table_saved_state_path": naming_table_saved_state_path}
252 def load_and_run(
253 self,
254 test_name: str,
255 variables: Mapping[str, str],
256 wait_for_server: bool = True,
257 use_serverless_ide: bool = False,
258 ) -> None:
259 test, expected = self.load_test_data(test_name, variables)
260 self.run_lsp_test(
261 test_name=test_name,
262 test=test,
263 expected=expected,
264 wait_for_server=wait_for_server,
265 use_serverless_ide=use_serverless_ide,
268 def run_spec(
269 self,
270 spec: LspTestSpec,
271 variables: Mapping[str, str],
272 wait_for_server: bool,
273 use_serverless_ide: bool,
274 ) -> None:
275 if wait_for_server:
276 assert not use_serverless_ide, (
277 "Warning: both `wait_for_server` and `use_serverless_ide` "
278 + "were set to `True` for testing in "
279 + self.run_lsp_test.__name__
280 + ". "
281 + "While this is a possible test case, it hasn't been written yet, "
282 + "so it's more likely that this is a mistake "
283 + "and you're accidentally relying on hh_server to fulfill "
284 + "serverless IDE requests."
285 + "(If you're writing that test, "
286 + "then it's time to remove this assertion.)"
289 # wait until hh_server is ready before starting lsp
290 self.test_driver.run_check()
291 elif use_serverless_ide:
292 self.test_driver.stop_hh_server()
294 with LspCommandProcessor.create(
295 self.test_driver.test_env, use_serverless_ide=use_serverless_ide
296 ) as lsp_command_processor:
297 (observed_transcript, error_details) = spec.run(
298 lsp_command_processor=lsp_command_processor, variables=variables
300 file = os.path.join(self.test_driver.template_repo, spec.name + ".sent.log")
301 text = json.dumps(
303 sent
304 for sent, _received in observed_transcript.values()
305 if sent is not None
307 indent=2,
309 with open(file, "w") as f:
310 f.write(text)
312 file = os.path.join(self.test_driver.template_repo, spec.name + ".received.log")
313 text = json.dumps(
315 received
316 for _sent, received in observed_transcript.values()
317 if received is not None
319 indent=2,
321 with open(file, "w") as f:
322 f.write(text)
324 if not use_serverless_ide:
325 # If the server's busy, maybe the machine's just under too much
326 # pressure to give results in a timely fashion. Doing a retry would
327 # only defer the question of what to do in that case, so instead
328 # we'll just skip.
329 self.throw_on_skip(observed_transcript)
331 if error_details is not None:
332 raise AssertionError(error_details)
334 def setup_php_file(self, test_php: str) -> Mapping[str, str]:
335 # We want the path to the builtins directory. This is best we can do.
336 (output, err, retcode) = self.test_driver.run_check(
337 options=["--identify-function", "2:21", "--json"],
338 stdin="<?hh // partial\nfunction f():void {PHP_EOL;}\n",
340 if retcode == 7:
341 self.skipTest(
342 "Could not discover builtins directory -- "
343 + "got exit code 7 (either Out_of_time or Out_of_retries). "
344 + "The test machine is likely under too much load."
346 self.assertEqual(retcode, 0)
347 constants_path = json.loads(output)[0]["definition_pos"]["filename"]
348 return {
349 "hhi_path": re.sub("/constants.hhi$", "", constants_path),
350 "root_path": self.test_driver.repo_dir,
351 "php_file_uri": self.repo_file_uri(test_php),
352 "php_file": self.read_repo_file(test_php),
355 def test_init_shutdown(self) -> None:
356 self.prepare_server_environment()
358 self.load_and_run(
359 "initialize_shutdown", {"root_path": self.test_driver.repo_dir}
362 def test_completion(self) -> None:
363 self.prepare_server_environment()
364 variables = self.setup_php_file("completion.php")
365 self.load_and_run("completion", variables)
367 def test_completion_legacy(self) -> None:
368 self.prepare_server_environment()
369 variables = self.setup_php_file("completion.php")
370 self.load_and_run("completion_legacy", variables)
372 def test_serverless_ide_definition(self) -> None:
373 variables = dict(self.prepare_serverless_ide_environment())
374 variables.update(self.setup_php_file("definition.php"))
375 self.test_driver.stop_hh_server()
377 spec = (
378 self.initialize_spec(
379 LspTestSpec("serverless_ide_definition"), use_serverless_ide=True
381 .notification(
382 method="textDocument/didOpen",
383 params={
384 "textDocument": {
385 "uri": "${php_file_uri}",
386 "languageId": "hack",
387 "version": 1,
388 "text": "${php_file}",
392 .request(
393 comment="call to `b_definition`",
394 method="textDocument/definition",
395 params={
396 "textDocument": {"uri": "${php_file_uri}"},
397 "position": {"line": 3, "character": 10},
399 result=[
401 "uri": "file://${root_path}/definition.php",
402 "range": {
403 "start": {"line": 6, "character": 9},
404 "end": {"line": 6, "character": 21},
406 "title": "b_definition",
409 powered_by="serverless_ide",
411 .request(
412 comment="call to `new BB(1)`",
413 method="textDocument/definition",
414 params={
415 "textDocument": {"uri": "${php_file_uri}"},
416 "position": {"line": 29, "character": 13},
418 result=[
420 "uri": "file://${root_path}/definition.php",
421 "range": {
422 "start": {"line": 11, "character": 18},
423 "end": {"line": 11, "character": 29},
425 "title": "BB::__construct",
428 powered_by="serverless_ide",
430 .request(
431 comment="call to `new CC(1)`",
432 method="textDocument/definition",
433 params={
434 "textDocument": {"uri": "${php_file_uri}"},
435 "position": {"line": 30, "character": 13},
437 result=[
439 "uri": "file://${root_path}/definition.php",
440 "range": {
441 "start": {"line": 14, "character": 6},
442 "end": {"line": 14, "character": 8},
444 "title": "CC",
447 "uri": "file://${root_path}/definition.php",
448 "range": {
449 "start": {"line": 11, "character": 18},
450 "end": {"line": 11, "character": 29},
452 "title": "BB::__construct",
455 powered_by="serverless_ide",
457 .request(
458 comment="call to `new DD(1)`",
459 method="textDocument/definition",
460 params={
461 "textDocument": {"uri": "${php_file_uri}"},
462 "position": {"line": 31, "character": 13},
464 result=[
466 "uri": "file://${root_path}/definition.php",
467 "range": {
468 "start": {"line": 17, "character": 6},
469 "end": {"line": 17, "character": 8},
471 "title": "DD",
474 "uri": "file://${root_path}/definition.php",
475 "range": {
476 "start": {"line": 11, "character": 18},
477 "end": {"line": 11, "character": 29},
479 "title": "BB::__construct",
482 powered_by="serverless_ide",
484 .request(
485 comment="call to `new EE(1)`",
486 method="textDocument/definition",
487 params={
488 "textDocument": {"uri": "${php_file_uri}"},
489 "position": {"line": 32, "character": 13},
491 result=[
493 "uri": "file://${root_path}/definition.php",
494 "range": {
495 "start": {"line": 21, "character": 18},
496 "end": {"line": 21, "character": 29},
498 "title": "EE::__construct",
501 powered_by="serverless_ide",
503 .request(
504 comment="call to `new FF(1)`",
505 method="textDocument/definition",
506 params={
507 "textDocument": {"uri": "${php_file_uri}"},
508 "position": {"line": 33, "character": 13},
510 result=[
512 "uri": "file://${root_path}/definition.php",
513 "range": {
514 "start": {"line": 26, "character": 6},
515 "end": {"line": 26, "character": 8},
517 "title": "FF",
520 powered_by="serverless_ide",
522 .request(
523 comment="call to `new TakesString(HasString::MyString)`",
524 method="textDocument/definition",
525 params={
526 "textDocument": {"uri": "${php_file_uri}"},
527 "position": {"line": 45, "character": 23},
529 result=[
531 "uri": "file://${root_path}/definition.php",
532 "range": {
533 "start": {"line": 40, "character": 6},
534 "end": {"line": 40, "character": 15},
536 "title": "HasString",
539 powered_by="serverless_ide",
541 .notification(
542 comment="make local, unsaved change to the file",
543 method="textDocument/didChange",
544 params={
545 "textDocument": {"uri": "${php_file_uri}", "version": 2},
546 "contentChanges": [
548 "text": "test",
549 "range": {
550 "start": {"line": 3, "character": 9},
551 "end": {"line": 3, "character": 21},
557 .request(
558 comment="call to `test` instead of `b_definition`",
559 method="textDocument/definition",
560 params={
561 "textDocument": {"uri": "${php_file_uri}"},
562 "position": {"line": 3, "character": 10},
564 result=[
566 "uri": "file://${root_path}/definition.php",
567 "range": {
568 "start": {"line": 28, "character": 9},
569 "end": {"line": 28, "character": 13},
571 "title": "test",
574 powered_by="serverless_ide",
576 .request(method="shutdown", params={}, result=None)
578 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
580 def test_serverless_ide_document_symbol(self) -> None:
581 variables = dict(self.prepare_serverless_ide_environment())
582 variables.update(self.setup_php_file("definition.php"))
583 self.test_driver.stop_hh_server()
585 spec = (
586 self.initialize_spec(
587 LspTestSpec("serverless_ide_document_symbol"), use_serverless_ide=True
589 .notification(
590 method="textDocument/didOpen",
591 params={
592 "textDocument": {
593 "uri": "${php_file_uri}",
594 "languageId": "hack",
595 "version": 1,
596 "text": "${php_file}",
600 .request(
601 comment="documentSymbol call",
602 method="textDocument/documentSymbol",
603 params={"textDocument": {"uri": "${php_file_uri}"}},
604 result=[
606 "name": "testClassMemberInsideConstructorInvocation",
607 "kind": 12,
608 "location": {
609 "uri": "file://${root_path}/definition.php",
610 "range": {
611 "start": {"line": 44, "character": 0},
612 "end": {"line": 46, "character": 1},
617 "name": "MyString",
618 "kind": 14,
619 "location": {
620 "uri": "file://${root_path}/definition.php",
621 "range": {
622 "start": {"line": 41, "character": 8},
623 "end": {"line": 41, "character": 29},
626 "containerName": "HasString",
629 "name": "HasString",
630 "kind": 5,
631 "location": {
632 "uri": "file://${root_path}/definition.php",
633 "range": {
634 "start": {"line": 40, "character": 0},
635 "end": {"line": 42, "character": 1},
640 "name": "__construct",
641 "kind": 6,
642 "location": {
643 "uri": "file://${root_path}/definition.php",
644 "range": {
645 "start": {"line": 37, "character": 2},
646 "end": {"line": 37, "character": 43},
649 "containerName": "TakesString",
652 "name": "TakesString",
653 "kind": 5,
654 "location": {
655 "uri": "file://${root_path}/definition.php",
656 "range": {
657 "start": {"line": 36, "character": 0},
658 "end": {"line": 38, "character": 1},
663 "name": "FF",
664 "kind": 5,
665 "location": {
666 "uri": "file://${root_path}/definition.php",
667 "range": {
668 "start": {"line": 26, "character": 0},
669 "end": {"line": 26, "character": 11},
674 "name": "__construct",
675 "kind": 6,
676 "location": {
677 "uri": "file://${root_path}/definition.php",
678 "range": {
679 "start": {"line": 21, "character": 2},
680 "end": {"line": 23, "character": 3},
683 "containerName": "EE",
686 "name": "EE",
687 "kind": 5,
688 "location": {
689 "uri": "file://${root_path}/definition.php",
690 "range": {
691 "start": {"line": 20, "character": 0},
692 "end": {"line": 24, "character": 1},
697 "name": "CC",
698 "kind": 5,
699 "location": {
700 "uri": "file://${root_path}/definition.php",
701 "range": {
702 "start": {"line": 14, "character": 0},
703 "end": {"line": 15, "character": 1},
708 "name": "__construct",
709 "kind": 6,
710 "location": {
711 "uri": "file://${root_path}/definition.php",
712 "range": {
713 "start": {"line": 11, "character": 2},
714 "end": {"line": 11, "character": 40},
717 "containerName": "BB",
720 "name": "BB",
721 "kind": 5,
722 "location": {
723 "uri": "file://${root_path}/definition.php",
724 "range": {
725 "start": {"line": 10, "character": 0},
726 "end": {"line": 12, "character": 1},
731 "name": "a_definition",
732 "kind": 12,
733 "location": {
734 "uri": "file://${root_path}/definition.php",
735 "range": {
736 "start": {"line": 2, "character": 0},
737 "end": {"line": 4, "character": 1},
742 "name": "b_definition",
743 "kind": 12,
744 "location": {
745 "uri": "file://${root_path}/definition.php",
746 "range": {
747 "start": {"line": 6, "character": 0},
748 "end": {"line": 8, "character": 1},
753 "name": "DD",
754 "kind": 5,
755 "location": {
756 "uri": "file://${root_path}/definition.php",
757 "range": {
758 "start": {"line": 17, "character": 0},
759 "end": {"line": 18, "character": 1},
764 "name": "test",
765 "kind": 12,
766 "location": {
767 "uri": "file://${root_path}/definition.php",
768 "range": {
769 "start": {"line": 28, "character": 0},
770 "end": {"line": 34, "character": 1},
775 powered_by="serverless_ide",
777 .request(method="shutdown", params={}, result=None)
779 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
781 def initialize_spec(
782 self, spec: LspTestSpec, use_serverless_ide: bool
783 ) -> LspTestSpec:
784 if use_serverless_ide:
785 initialization_options = {
786 "namingTableSavedStatePath": "${naming_table_saved_state_path}"
788 else:
789 initialization_options = {}
791 spec = spec.ignore_notifications(method="telemetry/event").request(
792 method="initialize",
793 params={
794 "initializationOptions": initialization_options,
795 "processId": None,
796 "rootPath": "${root_path}",
797 "capabilities": {},
799 result={
800 "capabilities": {
801 "textDocumentSync": {
802 "openClose": True,
803 "change": 2,
804 "willSave": False,
805 "willSaveWaitUntil": False,
806 "save": {"includeText": False},
808 "hoverProvider": True,
809 "completionProvider": {
810 "resolveProvider": True,
811 "triggerCharacters": ["$", ">", "\\", ":", "<", "[", "'", '"'],
813 "signatureHelpProvider": {"triggerCharacters": ["(", ","]},
814 "definitionProvider": True,
815 "typeDefinitionProvider": True,
816 "referencesProvider": True,
817 "documentHighlightProvider": True,
818 "documentSymbolProvider": True,
819 "workspaceSymbolProvider": True,
820 "codeActionProvider": False,
821 "documentFormattingProvider": True,
822 "documentRangeFormattingProvider": True,
823 "documentOnTypeFormattingProvider": {
824 "firstTriggerCharacter": ";",
825 "moreTriggerCharacter": ["}"],
827 "renameProvider": True,
828 "typeCoverageProvider": True,
829 "rageProvider": True,
833 if use_serverless_ide:
834 spec = spec.wait_for_server_request(
835 method="client/registerCapability",
836 params={
837 "registrations": [
839 "id": "did-change-watched-files",
840 "method": "workspace/didChangeWatchedFiles",
841 "registerOptions": {
842 "watchers": [{"globPattern": "**", "kind": 7}]
847 result=None,
849 return spec
851 def test_serverless_ide_type_definition(self) -> None:
852 variables = dict(self.prepare_serverless_ide_environment())
853 variables.update(self.setup_php_file("type_definition.php"))
854 self.test_driver.stop_hh_server()
856 spec = (
857 self.initialize_spec(
858 LspTestSpec("serverless_ide_type_definition"), use_serverless_ide=True
860 .notification(
861 method="textDocument/didOpen",
862 params={
863 "textDocument": {
864 "uri": "${php_file_uri}",
865 "languageId": "hack",
866 "version": 1,
867 "text": "${php_file}",
871 .request(
872 comment="Conditional Type Definition of HH or II",
873 method="textDocument/typeDefinition",
874 params={
875 "textDocument": {"uri": "${php_file_uri}"},
876 "position": {"line": 32, "character": 2},
878 result=[
880 "uri": "${php_file_uri}",
881 "range": {
882 "start": {"line": 2, "character": 6},
883 "end": {"line": 2, "character": 8},
885 "title": "\\HH",
888 "uri": "${php_file_uri}",
889 "range": {
890 "start": {"line": 12, "character": 6},
891 "end": {"line": 12, "character": 8},
893 "title": "\\LL",
896 powered_by="serverless_ide",
898 .request(
899 comment="Standard Class Definition",
900 method="textDocument/typeDefinition",
901 params={
902 "textDocument": {"uri": "${php_file_uri}"},
903 "position": {"line": 40, "character": 2},
905 result=[
907 "uri": "${php_file_uri}",
908 "range": {
909 "start": {"line": 2, "character": 6},
910 "end": {"line": 2, "character": 8},
912 "title": "\\HH",
915 powered_by="serverless_ide",
917 .request(
918 comment="Class Type Definition with Casting",
919 method="textDocument/typeDefinition",
920 params={
921 "textDocument": {"uri": "${php_file_uri}"},
922 "position": {"line": 41, "character": 2},
924 result=[
926 "uri": "${php_file_uri}",
927 "range": {
928 "start": {"line": 2, "character": 6},
929 "end": {"line": 2, "character": 8},
931 "title": "\\HH",
934 powered_by="serverless_ide",
936 .request(
937 comment="Primitive Type Definition",
938 method="textDocument/typeDefinition",
939 params={
940 "textDocument": {"uri": "${php_file_uri}"},
941 "position": {"line": 42, "character": 2},
943 result=[],
944 powered_by="serverless_ide",
946 .request(
947 comment="Function Return Type Definition",
948 method="textDocument/typeDefinition",
949 params={
950 "textDocument": {"uri": "${php_file_uri}"},
951 "position": {"line": 43, "character": 2},
953 result=[
955 "uri": "${php_file_uri}",
956 "range": {
957 "start": {"line": 12, "character": 6},
958 "end": {"line": 12, "character": 8},
960 "title": "\\LL",
963 powered_by="serverless_ide",
965 .request(
966 comment="Function definition with primitive return type",
967 method="textDocument/typeDefinition",
968 params={
969 "textDocument": {"uri": "${php_file_uri}"},
970 "position": {"line": 44, "character": 2},
972 result=[
974 "uri": "${php_file_uri}",
975 "range": {
976 "start": {"line": 22, "character": 9},
977 "end": {"line": 22, "character": 29},
979 "title": "(function(): int)",
982 powered_by="serverless_ide",
984 .request(method="shutdown", params={}, result=None)
986 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
988 def test_serverless_ide_hover(self) -> None:
989 variables = dict(self.prepare_serverless_ide_environment())
990 variables.update(self.setup_php_file("hover.php"))
991 self.test_driver.stop_hh_server()
993 spec = (
994 self.initialize_spec(
995 LspTestSpec("serverless_ide_hover"), use_serverless_ide=True
997 .notification(
998 method="textDocument/didOpen",
999 params={
1000 "textDocument": {
1001 "uri": "${php_file_uri}",
1002 "languageId": "hack",
1003 "version": 1,
1004 "text": "${php_file}",
1008 .request(
1009 comment="hover over function invocation",
1010 method="textDocument/hover",
1011 params={
1012 "textDocument": {"uri": "${php_file_uri}"},
1013 "position": {"line": 3, "character": 16},
1015 result={
1016 "contents": [
1017 {"language": "hack", "value": "int"},
1018 "A comment describing b_hover.",
1020 "range": {
1021 "start": {"line": 3, "character": 9},
1022 "end": {"line": 3, "character": 16},
1025 powered_by="serverless_ide",
1027 .request(
1028 comment="hover over whitespace",
1029 method="textDocument/hover",
1030 params={
1031 "textDocument": {"uri": "${php_file_uri}"},
1032 "position": {"line": 3, "character": 1},
1034 result=None,
1035 powered_by="serverless_ide",
1037 .request(
1038 comment="hover over a keyword",
1039 method="textDocument/hover",
1040 params={
1041 "textDocument": {"uri": "${php_file_uri}"},
1042 "position": {"line": 2, "character": 1},
1044 result=None,
1045 powered_by="serverless_ide",
1047 .request(
1048 comment="hover over a comment",
1049 method="textDocument/hover",
1050 params={
1051 "textDocument": {"uri": "${php_file_uri}"},
1052 "position": {"line": 1, "character": 4},
1054 result=None,
1055 powered_by="serverless_ide",
1057 .request(
1058 comment="hover past the end of a line",
1059 method="textDocument/hover",
1060 params={
1061 "textDocument": {"uri": "${php_file_uri}"},
1062 "position": {"line": 3, "character": 100},
1064 result=None,
1065 powered_by="serverless_ide",
1067 .request(
1068 comment="hover past the end of a file",
1069 method="textDocument/hover",
1070 params={
1071 "textDocument": {"uri": "${php_file_uri}"},
1072 "position": {"line": 300, "character": 0},
1074 result=None,
1075 powered_by="serverless_ide",
1077 .request(method="shutdown", params={}, result=None)
1079 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
1081 def test_serverless_ide_file_touched_on_disk(self) -> None:
1082 variables = dict(self.prepare_serverless_ide_environment())
1083 variables.update(self.setup_php_file("hover.php"))
1084 self.test_driver.stop_hh_server()
1086 spec = (
1087 self.initialize_spec(
1088 LspTestSpec("serverless_ide_file_on_disk_change"),
1089 use_serverless_ide=True,
1091 .notification(
1092 method="textDocument/didOpen",
1093 params={
1094 "textDocument": {
1095 "uri": "${php_file_uri}",
1096 "languageId": "hack",
1097 "version": 1,
1098 "text": "${php_file}",
1102 .notification(
1103 method="workspace/didChangeWatchedFiles",
1104 params={"changes": [{"uri": "${php_file_uri}", "type": 2}]},
1106 .wait_for_notification(
1107 comment="wait for sIDE to process file change",
1108 method="telemetry/event",
1109 params={
1110 "type": 4,
1111 "message": "[client-ide] Done processing file changes",
1114 .request(
1115 method="textDocument/hover",
1116 params={
1117 "textDocument": {"uri": "${php_file_uri}"},
1118 "position": {"line": 3, "character": 16},
1120 result={
1121 "contents": [
1122 {"language": "hack", "value": "int"},
1123 "A comment describing b_hover.",
1125 "range": {
1126 "start": {"line": 3, "character": 9},
1127 "end": {"line": 3, "character": 16},
1130 powered_by="serverless_ide",
1132 .request(method="shutdown", params={}, result=None)
1134 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
1136 def test_serverless_ide_file_hover_with_errors(self) -> None:
1137 variables = dict(self.prepare_serverless_ide_environment())
1138 variables.update(self.setup_php_file("hover_with_errors.php"))
1139 self.test_driver.stop_hh_server()
1141 spec = (
1142 self.initialize_spec(
1143 LspTestSpec("serverless_ide_hover_with_errors"), use_serverless_ide=True
1145 .notification(
1146 method="textDocument/didOpen",
1147 params={
1148 "textDocument": {
1149 "uri": "${php_file_uri}",
1150 "languageId": "hack",
1151 "version": 1,
1152 "text": "${php_file}",
1156 .notification(
1157 method="workspace/didChangeWatchedFiles",
1158 params={"changes": [{"uri": "${php_file_uri}", "type": 2}]},
1160 .wait_for_notification(
1161 comment="wait for sIDE to process file change",
1162 method="telemetry/event",
1163 params={
1164 "type": 4,
1165 "message": "[client-ide] Done processing file changes",
1168 .request(
1169 comment="Totally normal hover",
1170 method="textDocument/hover",
1171 params={
1172 "textDocument": {"uri": "${php_file_uri}"},
1173 "position": {"line": 14, "character": 37},
1175 result={
1176 "contents": [
1178 "language": "hack",
1179 "value": "public static function staticMethod(string $z): void",
1181 'During testing, we\'ll remove the "public" tag from this '
1182 "method\n"
1183 "to ensure that we can still get IDE services",
1184 "Return type: `void`",
1185 "Full name: `HoverWithErrorsClass::staticMethod`",
1187 "range": {
1188 "end": {"character": 39, "line": 14},
1189 "start": {"character": 27, "line": 14},
1192 powered_by="serverless_ide",
1194 .notification(
1195 comment="Remove the 'public' visibility modifier which triggers AST->AAST errors",
1196 method="textDocument/didChange",
1197 params={
1198 "textDocument": {"uri": "${php_file_uri}"},
1199 "contentChanges": [
1201 "range": {
1202 "start": {"line": 10, "character": 2},
1203 "end": {"line": 10, "character": 8},
1205 "text": "",
1210 .request(
1211 comment="Hover should still work even if visibility modifier has been removed",
1212 method="textDocument/hover",
1213 params={
1214 "textDocument": {"uri": "${php_file_uri}"},
1215 "position": {"line": 14, "character": 37},
1217 result={
1218 "contents": [
1220 "language": "hack",
1221 "value": "public static function staticMethod(string $z): void",
1223 'During testing, we\'ll remove the "public" tag from this '
1224 "method\n"
1225 "to ensure that we can still get IDE services",
1226 "Return type: `void`",
1227 "Full name: `HoverWithErrorsClass::staticMethod`",
1229 "range": {
1230 "end": {"character": 39, "line": 14},
1231 "start": {"character": 27, "line": 14},
1234 powered_by="serverless_ide",
1236 .request(method="shutdown", params={}, result=None)
1238 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
1240 def test_formatting(self) -> None:
1242 # This test will fail if hackfmt can't be found
1243 if not self.test_driver.run_hackfmt_check():
1244 raise unittest.SkipTest("Hackfmt can't be found. Skipping.")
1246 self.prepare_server_environment()
1247 variables = self.setup_php_file("messy.php")
1248 self.load_and_run("formatting", variables)
1250 def test_rangeformatting(self) -> None:
1251 # This test will fail if hackfmt can't be found
1252 if not self.test_driver.run_hackfmt_check():
1253 raise unittest.SkipTest("Hackfmt can't be found. Skipping.")
1255 self.prepare_server_environment()
1256 variables = self.setup_php_file("messy.php")
1257 spec = (
1258 self.initialize_spec(
1259 LspTestSpec("range_formatting"), use_serverless_ide=False
1261 .wait_for_hh_server_ready()
1262 .notification(
1263 method="textDocument/didOpen",
1264 params={
1265 "textDocument": {
1266 "uri": "${php_file_uri}",
1267 "languageId": "hack",
1268 "version": 1,
1269 "text": "${php_file}",
1273 .request(
1274 method="textDocument/rangeFormatting",
1275 params={
1276 "textDocument": {"uri": "${php_file_uri}"},
1277 "range": {
1278 "start": {"line": 4, "character": 0},
1279 "end": {"line": 5, "character": 0},
1281 "options": {"tabSize": 5, "insertSpaces": True},
1283 result=[
1285 "range": {
1286 "start": {"line": 4, "character": 0},
1287 "end": {"line": 5, "character": 0},
1289 "newText": ' $a = "this";\n',
1293 .request(method="shutdown", params={}, result=None)
1295 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=False)
1297 def test_ontypeformatting(self) -> None:
1299 # This test will fail if hackfmt can't be found
1300 if not self.test_driver.run_hackfmt_check():
1301 raise unittest.SkipTest("Hackfmt can't be found. Skipping.")
1303 self.prepare_server_environment()
1304 variables = self.setup_php_file("ontypeformatting.php")
1305 self.load_and_run("ontypeformatting", variables)
1307 def test_did_change(self) -> None:
1308 self.prepare_server_environment()
1309 variables = self.setup_php_file("didchange.php")
1310 spec = (
1311 self.initialize_spec(LspTestSpec("did_change"), use_serverless_ide=False)
1312 .wait_for_hh_server_ready()
1313 .notification(
1314 method="textDocument/didOpen",
1315 params={
1316 "textDocument": {
1317 "uri": "${php_file_uri}",
1318 "languageId": "hack",
1319 "version": 1,
1320 "text": "${php_file}",
1324 .notification(
1325 method="textDocument/didChange",
1326 params={
1327 "textDocument": {"uri": "${php_file_uri}"},
1328 "contentChanges": [
1330 "range": {
1331 "start": {"line": 7, "character": 11},
1332 "end": {"line": 7, "character": 12},
1334 "text": "a",
1339 .wait_for_notification(
1340 method="textDocument/publishDiagnostics",
1341 params={
1342 "uri": "${php_file_uri}",
1343 "diagnostics": [
1345 "range": {
1346 "start": {"line": 7, "character": 11},
1347 "end": {"line": 7, "character": 11},
1349 "severity": 1,
1350 "code": 1002,
1351 "source": "Hack",
1352 "message": "A semicolon (';') is expected here.",
1353 "relatedLocations": [],
1354 "relatedInformation": [],
1359 .request(method="shutdown", params={}, result=None)
1360 .wait_for_notification(
1361 comment="Hack appears to clear out diagnostics before shutting down",
1362 method="textDocument/publishDiagnostics",
1363 params={"uri": "${php_file_uri}", "diagnostics": []},
1366 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=False)
1368 def test_signature_help(self) -> None:
1369 self.prepare_server_environment()
1370 variables = self.setup_php_file("signaturehelp.php")
1371 spec = (
1372 self.initialize_spec(
1373 LspTestSpec("test_signature_help"), use_serverless_ide=False
1375 .wait_for_hh_server_ready()
1376 .notification(
1377 method="textDocument/didOpen",
1378 params={
1379 "textDocument": {
1380 "uri": "${php_file_uri}",
1381 "languageId": "hack",
1382 "version": 1,
1383 "text": "${php_file}",
1387 .request(
1388 comment="signature help for 0-argument constructor (left of opening paren)",
1389 method="textDocument/signatureHelp",
1390 params={
1391 "textDocument": {"uri": "${php_file_uri}"},
1392 "position": {"line": 16, "character": 18},
1394 result=None,
1396 .request(
1397 comment="signature help for 0-argument constructor",
1398 method="textDocument/signatureHelp",
1399 params={
1400 "textDocument": {"uri": "${php_file_uri}"},
1401 "position": {"line": 16, "character": 19},
1403 result={
1404 "signatures": [
1406 "label": "public function __construct(): _",
1407 "documentation": "Constructor with doc block",
1408 "parameters": [],
1411 "activeSignature": 0,
1412 "activeParameter": 0,
1415 .request(
1416 comment="signature help for 0-argument constructor (right of closing paren)",
1417 method="textDocument/signatureHelp",
1418 params={
1419 "textDocument": {"uri": "${php_file_uri}"},
1420 "position": {"line": 16, "character": 20},
1422 result=None,
1424 .request(
1425 comment="signature help for 2-argument instance method (left of opening paren)",
1426 method="textDocument/signatureHelp",
1427 params={
1428 "textDocument": {"uri": "${php_file_uri}"},
1429 "position": {"line": 17, "character": 20},
1431 result=None,
1433 .request(
1434 comment="signature help for 2-argument instance method (right of opening paren)",
1435 method="textDocument/signatureHelp",
1436 params={
1437 "textDocument": {"uri": "${php_file_uri}"},
1438 "position": {"line": 17, "character": 21},
1440 result={
1441 "signatures": [
1443 "label": "public function instanceMethod(int $x1, int $x2): void",
1444 "documentation": "Instance method with doc block",
1445 "parameters": [{"label": "$x1"}, {"label": "$x2"}],
1448 "activeSignature": 0,
1449 "activeParameter": 0,
1452 .request(
1453 comment="signature help for 2-argument instance method (left of first comma)",
1454 method="textDocument/signatureHelp",
1455 params={
1456 "textDocument": {"uri": "${php_file_uri}"},
1457 "position": {"line": 17, "character": 22},
1459 result={
1460 "signatures": [
1462 "label": "public function instanceMethod(int $x1, int $x2): void",
1463 "documentation": "Instance method with doc block",
1464 "parameters": [{"label": "$x1"}, {"label": "$x2"}],
1467 "activeSignature": 0,
1468 "activeParameter": 1,
1471 .request(
1472 comment="signature help for 2-argument instance method (right of first comma)",
1473 method="textDocument/signatureHelp",
1474 params={
1475 "textDocument": {"uri": "${php_file_uri}"},
1476 "position": {"line": 17, "character": 23},
1478 result={
1479 "signatures": [
1481 "label": "public function instanceMethod(int $x1, int $x2): void",
1482 "documentation": "Instance method with doc block",
1483 "parameters": [{"label": "$x1"}, {"label": "$x2"}],
1486 "activeSignature": 0,
1487 "activeParameter": 1,
1490 .request(
1491 comment="signature help for 2-argument instance method (left of closing paren)",
1492 method="textDocument/signatureHelp",
1493 params={
1494 "textDocument": {"uri": "${php_file_uri}"},
1495 "position": {"line": 17, "character": 24},
1497 result={
1498 "signatures": [
1500 "label": "public function instanceMethod(int $x1, int $x2): void",
1501 "documentation": "Instance method with doc block",
1502 "parameters": [{"label": "$x1"}, {"label": "$x2"}],
1505 "activeSignature": 0,
1506 "activeParameter": 1,
1509 .request(
1510 comment="signature help for 2-argument instance method (right of closing paren)",
1511 method="textDocument/signatureHelp",
1512 params={
1513 "textDocument": {"uri": "${php_file_uri}"},
1514 "position": {"line": 17, "character": 25},
1516 result=None,
1518 .request(
1519 comment="signature help for 1-argument static method (left of open paren)",
1520 method="textDocument/signatureHelp",
1521 params={
1522 "textDocument": {"uri": "${php_file_uri}"},
1523 "position": {"line": 18, "character": 23},
1525 result=None,
1527 .request(
1528 comment="signature help for 1-argument static method (right of open paren)",
1529 method="textDocument/signatureHelp",
1530 params={
1531 "textDocument": {"uri": "${php_file_uri}"},
1532 "position": {"line": 18, "character": 24},
1534 result={
1535 "signatures": [
1537 "label": "public static function staticMethod(string $z): void",
1538 "documentation": "Static method with doc block",
1539 "parameters": [{"label": "$z"}],
1542 "activeSignature": 0,
1543 "activeParameter": 0,
1546 .request(
1547 comment="signature help for 2-argument global function (left of open paren)",
1548 method="textDocument/signatureHelp",
1549 params={
1550 "textDocument": {"uri": "${php_file_uri}"},
1551 "position": {"line": 19, "character": 17},
1553 result=None,
1555 .request(
1556 comment="signature help for 2-argument global function (right of open paren)",
1557 method="textDocument/signatureHelp",
1558 params={
1559 "textDocument": {"uri": "${php_file_uri}"},
1560 "position": {"line": 19, "character": 18},
1562 result={
1563 "signatures": [
1565 "label": "function global_function(string $s, int $x): void",
1566 "documentation": "Global function with doc block",
1567 "parameters": [{"label": "$s"}, {"label": "$x"}],
1570 "activeSignature": 0,
1571 "activeParameter": 0,
1574 .request(
1575 comment="signature help for 1-argument namespace-aliased global function (left of open paren)",
1576 method="textDocument/signatureHelp",
1577 params={
1578 "textDocument": {"uri": "${php_file_uri}"},
1579 "position": {"line": 20, "character": 26},
1581 result=None,
1583 .request(
1584 comment="signature help for 1-argument namespace-aliased global function (right of open paren)",
1585 method="textDocument/signatureHelp",
1586 params={
1587 "textDocument": {"uri": "${php_file_uri}"},
1588 "position": {"line": 20, "character": 26},
1590 result=None,
1592 .request(
1593 comment="signature help for 1-argument namespace-aliased global function (right of open paren)",
1594 method="textDocument/signatureHelp",
1595 params={
1596 "textDocument": {"uri": "${php_file_uri}"},
1597 "position": {"line": 20, "character": 27},
1599 result={
1600 "signatures": [
1602 "label": "function Herp\\aliased_global_func(string $s): void",
1603 "parameters": [{"label": "$s"}],
1606 "activeSignature": 0,
1607 "activeParameter": 0,
1610 .request(
1611 comment="signature help for 1-argument namespace-aliased global function (right of open paren)",
1612 method="textDocument/signatureHelp",
1613 params={
1614 "textDocument": {"uri": "${php_file_uri}"},
1615 "position": {"line": 20, "character": 28},
1617 result={
1618 "signatures": [
1620 "label": "function Herp\\aliased_global_func(string $s): void",
1621 "parameters": [{"label": "$s"}],
1624 "activeSignature": 0,
1625 "activeParameter": 0,
1628 .request(
1629 comment="signature help for 2-argument function with params (right of open paren)",
1630 method="textDocument/signatureHelp",
1631 params={
1632 "textDocument": {"uri": "${php_file_uri}"},
1633 "position": {"line": 21, "character": 30},
1635 result={
1636 "signatures": [
1638 "label": "function test_signature_help_params1(\n string $param1,\n string $param2\n): void",
1639 "documentation": "comment describing the method\n@param $param1 info1\n@param $param2 info2",
1640 "parameters": [
1641 {"label": "$param1", "documentation": "info1"},
1642 {"label": "$param2", "documentation": "info2"},
1646 "activeSignature": 0,
1647 "activeParameter": 0,
1650 .request(
1651 comment="signature help for 2-argument function with params (right of open paren)",
1652 method="textDocument/signatureHelp",
1653 params={
1654 "textDocument": {"uri": "${php_file_uri}"},
1655 "position": {"line": 22, "character": 30},
1657 result={
1658 "signatures": [
1660 "label": "function test_signature_help_params2(\n string $param1,\n string $param2\n): void",
1661 "documentation": "comment describing the method\n@param $param1 info1",
1662 "parameters": [
1663 {"label": "$param1", "documentation": "info1"},
1664 {"label": "$param2"},
1668 "activeSignature": 0,
1669 "activeParameter": 0,
1672 .request(
1673 comment="signature help for 2-argument function with params (right of open paren)",
1674 method="textDocument/signatureHelp",
1675 params={
1676 "textDocument": {"uri": "${php_file_uri}"},
1677 "position": {"line": 23, "character": 30},
1679 result={
1680 "signatures": [
1682 "label": "function test_signature_help_params3(\n string $param1,\n string $param2\n): string",
1683 "documentation": "@param $param1 info1\n for param1\n@param $param2 info2\n@return the string\n 'hack'",
1684 "parameters": [
1686 "label": "$param1",
1687 "documentation": "info1 for param1",
1689 {"label": "$param2", "documentation": "info2"},
1693 "activeSignature": 0,
1694 "activeParameter": 0,
1697 .request(method="shutdown", params={}, result=None)
1699 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=False)
1701 def test_rename(self) -> None:
1702 self.prepare_server_environment()
1703 variables = self.setup_php_file("rename.php")
1704 self.load_and_run("rename", variables)
1706 def test_references(self) -> None:
1707 self.prepare_server_environment()
1708 variables = self.setup_php_file("references.php")
1709 self.load_and_run("references", variables)
1711 def test_non_existing_method(self) -> None:
1712 self.prepare_server_environment()
1713 variables = self.setup_php_file("nomethod.php")
1714 self.load_and_run("nomethod", variables)
1716 def test_bad_call(self) -> None:
1717 self.prepare_server_environment()
1718 variables = self.setup_php_file("bad_call.php")
1719 self.load_and_run("bad_call", variables)
1721 def test_non_blocking(self) -> None:
1722 self.prepare_server_environment()
1723 variables = self.setup_php_file("non_blocking.php")
1724 self.test_driver.start_hh_loop_forever_assert_timeout()
1725 spec = (
1726 self.initialize_spec(LspTestSpec("non_blocking"), use_serverless_ide=False)
1727 .wait_for_hh_server_ready()
1728 .request(
1729 method="textDocument/definition",
1730 params={
1731 "textDocument": {"uri": "${php_file_uri}"},
1732 "position": {"line": 7, "character": 11},
1734 result=[
1736 "uri": "file://${root_path}/non_blocking.php",
1737 "range": {
1738 "start": {"line": 2, "character": 9},
1739 "end": {"line": 2, "character": 32},
1741 "title": "non_blocking_definition",
1744 wait_id="definition request",
1746 .notification(
1747 comment="remove hh_loop_forever() invocation to break the infinite loop",
1748 method="textDocument/didOpen",
1749 params={
1750 "textDocument": {
1751 "uri": "${root_path}/__hh_loop_forever_foo.php",
1752 "languageId": "hack",
1753 "version": 1,
1754 "text": """\
1755 <?hh // strict
1757 function __hh_loop_forever_foo(): int {
1758 return 4;
1760 """,
1764 .wait_for_response(wait_id="definition request")
1765 .request(method="shutdown", params={}, result=None)
1767 self.run_spec(spec, variables, wait_for_server=True, use_serverless_ide=False)
1769 def test_serverless_ide_hierarchy_file_change_on_disk(self) -> None:
1770 variables = dict(self.prepare_serverless_ide_environment())
1771 variables.update(self.setup_php_file("incremental_derived.php"))
1772 changed_php_file_uri = self.repo_file("incremental_base.php")
1773 variables.update({"changed_php_file_uri": changed_php_file_uri})
1774 self.test_driver.stop_hh_server()
1776 spec = (
1777 self.initialize_spec(
1778 LspTestSpec("serverless_ide_hierarchy_file_change_on_disk"),
1779 use_serverless_ide=True,
1781 .notification(
1782 method="textDocument/didOpen",
1783 params={
1784 "textDocument": {
1785 "uri": "${php_file_uri}",
1786 "languageId": "hack",
1787 "version": 1,
1788 "text": "${php_file}",
1792 .request(
1793 comment="hover before change to class hierarchy should be `int`",
1794 method="textDocument/hover",
1795 params={
1796 "textDocument": {"uri": "${php_file_uri}"},
1797 "position": {"line": 7, "character": 14},
1799 result={
1800 "contents": [
1801 {"language": "hack", "value": "public function foo(): int"},
1802 "Return type: `int`",
1803 "Full name: `BaseClassIncremental::foo`",
1805 "range": {
1806 "start": {"line": 7, "character": 12},
1807 "end": {"line": 7, "character": 15},
1810 powered_by="serverless_ide",
1812 .write_to_disk(
1813 uri=changed_php_file_uri,
1814 contents="""\
1815 <?hh // strict
1816 class BaseClassIncremental {
1817 public function foo(): string { return ''; }
1819 """,
1820 notify=True,
1822 .wait_for_notification(
1823 comment="wait for sIDE to process file change",
1824 method="telemetry/event",
1825 params={
1826 "type": 4,
1827 "message": "[client-ide] Done processing file changes",
1830 .request(
1831 comment="hover after change to class hierarchy should be `string`",
1832 method="textDocument/hover",
1833 params={
1834 "textDocument": {"uri": "${php_file_uri}"},
1835 "position": {"line": 7, "character": 14},
1837 result={
1838 "contents": [
1839 {"language": "hack", "value": "public function foo(): string"},
1840 "Return type: `string`",
1841 "Full name: `BaseClassIncremental::foo`",
1843 "range": {
1844 "start": {"line": 7, "character": 12},
1845 "end": {"line": 7, "character": 15},
1848 powered_by="serverless_ide",
1850 .request(method="shutdown", params={}, result=None)
1853 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
1855 def test_serverless_ide_decl_in_unsaved_buffer_changed(self) -> None:
1856 variables = dict(self.prepare_serverless_ide_environment())
1857 variables.update(self.setup_php_file("hover.php"))
1858 self.test_driver.stop_hh_server()
1860 spec = (
1861 self.initialize_spec(
1862 LspTestSpec("serverless_ide_decl_in_unsaved_buffer_changed"),
1863 use_serverless_ide=True,
1865 .notification(
1866 method="textDocument/didOpen",
1867 params={
1868 "textDocument": {
1869 "uri": "${php_file_uri}",
1870 "languageId": "hack",
1871 "version": 1,
1872 "text": "${php_file}",
1876 .request(
1877 comment="hover over function invocation",
1878 method="textDocument/hover",
1879 params={
1880 "textDocument": {"uri": "${php_file_uri}"},
1881 "position": {"line": 3, "character": 16},
1883 result={
1884 "contents": [
1885 {"language": "hack", "value": "int"},
1886 "A comment describing b_hover.",
1888 "range": {
1889 "start": {"line": 3, "character": 9},
1890 "end": {"line": 3, "character": 16},
1893 powered_by="serverless_ide",
1895 .notification(
1896 comment="make local, unsaved change to the file",
1897 method="textDocument/didChange",
1898 params={
1899 "textDocument": {"uri": "${php_file_uri}", "version": 2},
1900 "contentChanges": [
1902 "text": """\
1903 <?hh // strict
1904 // comment
1905 function a_hover(): int {
1906 return b_hover();
1908 # A comment describing b_hover.
1909 function b_hover(): string {
1910 return 42;
1917 .request(
1918 comment="another hover over function invocation, should be string now",
1919 method="textDocument/hover",
1920 params={
1921 "textDocument": {"uri": "${php_file_uri}"},
1922 "position": {"line": 3, "character": 16},
1924 result={
1925 "contents": [
1926 {"language": "hack", "value": "string"},
1927 "A comment describing b_hover.",
1929 "range": {
1930 "start": {"line": 3, "character": 9},
1931 "end": {"line": 3, "character": 16},
1934 powered_by="serverless_ide",
1936 .request(method="shutdown", params={}, result=None)
1939 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
1941 def _sanitize_gutter_line_numbers(self, s: str) -> str:
1942 gutter_line_number_re = re.compile(r"^[ ]*[0-9]+ \|", re.MULTILINE)
1943 return re.sub(gutter_line_number_re, " XXXX |", s)
1945 def test_lsptestspec_incorrect_request_result(self) -> None:
1946 variables = dict(self.prepare_serverless_ide_environment())
1947 variables.update(self.setup_php_file("hover.php"))
1948 self.test_driver.stop_hh_server()
1950 spec = (
1951 self.initialize_spec(LspTestSpec("bad_hover"), use_serverless_ide=True)
1952 .notification(
1953 method="textDocument/didOpen",
1954 params={
1955 "textDocument": {
1956 "uri": "${php_file_uri}",
1957 "languageId": "hack",
1958 "version": 1,
1959 "text": "${php_file}",
1963 .request(
1964 comment="hover over function invocation",
1965 method="textDocument/hover",
1966 params={
1967 "textDocument": {"uri": "${php_file_uri}"},
1968 "position": {"line": 3, "character": 16},
1970 result={
1971 "contents": [
1972 {"language": "hack", "value": "int"},
1973 "INCORRECT COMMENT HERE",
1975 "range": {
1976 "start": {"line": 3, "character": 9},
1977 "end": {"line": 3, "character": 16},
1980 powered_by="serverless_ide",
1982 .request(method="shutdown", params={}, result=None)
1984 try:
1985 self.run_spec(
1986 spec,
1987 variables=variables,
1988 wait_for_server=False,
1989 use_serverless_ide=True,
1991 assert False, "No assertion failure raised"
1992 except AssertionError as e:
1993 self.assertEqual(
1994 self._sanitize_gutter_line_numbers(str(e)),
1995 """\
1996 Test case bad_hover failed with 1 errors:
1998 Error 1/1:
1999 Description: Request with ID 4 (comment: 'hover over function invocation') \
2000 got an incorrect result:
2002 (+ is expected lines, - is actual lines)
2003 - {'contents': [{'language': 'hack', 'value': 'int'},
2004 + {'contents': [{'language': 'hack', 'value': 'int'}, 'INCORRECT COMMENT HERE'],
2005 ? +++++++++++++++++++++++++++
2007 - 'A comment describing b_hover.'],
2008 'range': {'end': {'character': 16, 'line': 3},
2009 'start': {'character': 9, 'line': 3}}}
2011 Context:
2012 This was the associated request:
2014 hphp/hack/test/integration/test_lsp.py
2015 XXXX | .request(
2016 XXXX | comment="hover over function invocation",
2017 XXXX | method="textDocument/hover",
2018 XXXX | params={
2019 XXXX | "textDocument": {"uri": "${php_file_uri}"},
2020 XXXX | "position": {"line": 3, "character": 16},
2021 XXXX | },
2022 XXXX | result={
2023 XXXX | "contents": [
2024 XXXX | {"language": "hack", "value": "int"},
2025 XXXX | "INCORRECT COMMENT HERE",
2026 XXXX | ],
2027 XXXX | "range": {
2028 XXXX | "start": {"line": 3, "character": 9},
2029 XXXX | "end": {"line": 3, "character": 16},
2030 XXXX | },
2031 XXXX | },
2032 XXXX | powered_by="serverless_ide",
2033 XXXX | )
2035 Remediation:
2036 1) If this was unexpected, then the language server is buggy and should be
2037 fixed.
2039 2) If this was expected, you can update your request with the following code to
2040 make it match:
2042 .request(
2043 comment='hover over function invocation',
2044 method='textDocument/hover',
2045 params={'textDocument': {'uri': '${php_file_uri}'}, \
2046 'position': {'line': 3, 'character': 16}},
2047 result={'contents': [{'language': 'hack', 'value': 'int'}, \
2048 'A comment describing b_hover.'], \
2049 'range': {'start': {'line': 3, 'character': 9}, \
2050 'end': {'line': 3, 'character': 16}}},
2051 powered_by='serverless_ide',
2054 If you want to examine the raw LSP logs, you can check the `.sent.log` and
2055 `.received.log` files that were generated in the template repo for this test.\
2056 """,
2059 def test_lsptestspec_unexpected_notification(self) -> None:
2060 self.prepare_server_environment()
2061 variables = self.setup_php_file("didchange.php")
2062 spec = (
2063 self.initialize_spec(LspTestSpec("did_change"), use_serverless_ide=False)
2064 .wait_for_hh_server_ready()
2065 .notification(
2066 method="textDocument/didOpen",
2067 params={
2068 "textDocument": {
2069 "uri": "${php_file_uri}",
2070 "languageId": "hack",
2071 "version": 1,
2072 "text": "${php_file}",
2076 .notification(
2077 method="textDocument/didChange",
2078 params={
2079 "textDocument": {"uri": "${php_file_uri}"},
2080 "contentChanges": [
2082 "range": {
2083 "start": {"line": 7, "character": 11},
2084 "end": {"line": 7, "character": 12},
2086 "text": "a",
2091 .wait_for_notification(
2092 method="textDocument/publishDiagnostics",
2093 params={
2094 "uri": "${php_file_uri}",
2095 "diagnostics": [
2097 "range": {
2098 "start": {"line": 7, "character": 11},
2099 "end": {"line": 7, "character": 11},
2101 "severity": 1,
2102 "code": 1002,
2103 "source": "Hack",
2104 "message": "A semicolon (';') is expected here.",
2105 "relatedLocations": [],
2106 "relatedInformation": [],
2111 .request(method="shutdown", params={}, result=None)
2113 try:
2114 self.run_spec(
2115 spec, variables, wait_for_server=True, use_serverless_ide=False
2117 assert False, "No assertion failure raised"
2118 except AssertionError as e:
2119 self.assertEqual(
2120 self._sanitize_gutter_line_numbers(str(e)),
2121 """\
2122 Test case did_change failed with 1 errors:
2124 Error 1/1:
2125 Description: An unexpected notification of type \
2126 'textDocument/publishDiagnostics' was sent by the language server.
2127 Here is the notification payload:
2129 {'jsonrpc': '2.0',
2130 'method': 'textDocument/publishDiagnostics',
2131 'params': {'diagnostics': [],
2132 'uri': '__PHP_FILE_URI__'}}
2134 Context:
2135 This was the most recent request issued from the language client before it
2136 received the notification:
2138 hphp/hack/test/integration/test_lsp.py
2139 XXXX | .request(method="shutdown", params={}, result=None)
2141 Remediation:
2142 1) If this was unexpected, then the language server is buggy and should be
2143 fixed.
2145 2) If all notifications of type 'textDocument/publishDiagnostics' should be \
2146 ignored, add this directive
2147 anywhere in your test:
2149 .ignore_notifications(method='textDocument/publishDiagnostics')
2151 3) If this single instance of the notification was expected, add this directive
2152 to your test to wait for it before proceeding:
2154 .wait_for_notification(
2155 method='textDocument/publishDiagnostics',
2156 params={'uri': '${php_file_uri}', 'diagnostics': []},
2159 If you want to examine the raw LSP logs, you can check the `.sent.log` and
2160 `.received.log` files that were generated in the template repo for this test.\
2162 # There's an instance of a literal `${php_file_uri}` in there
2163 # which we don't want to change, so use a different name than
2164 # that one.
2165 .replace("__PHP_FILE_URI__", variables["php_file_uri"]),
2168 def test_serverless_ide_highlight(self) -> None:
2169 variables = dict(self.prepare_serverless_ide_environment())
2170 variables.update(self.setup_php_file("highlight.php"))
2171 self.test_driver.stop_hh_server()
2173 spec = (
2174 self.initialize_spec(
2175 LspTestSpec("serverless_ide_highlight"), use_serverless_ide=True
2177 .notification(
2178 method="textDocument/didOpen",
2179 params={
2180 "textDocument": {
2181 "uri": "${php_file_uri}",
2182 "languageId": "hack",
2183 "version": 1,
2184 "text": "${php_file}",
2188 .request(
2189 comment="document highlight, id 2",
2190 method="textDocument/documentHighlight",
2191 params={
2192 "textDocument": {"uri": "${php_file_uri}"},
2193 "position": {"line": 3, "character": 10},
2195 result=[
2197 "range": {
2198 "start": {"line": 3, "character": 9},
2199 "end": {"line": 3, "character": 20},
2204 .request(
2205 comment="shutdown, id 3", method="shutdown", params={}, result=None
2208 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)
2210 def test_serverless_ide_coverage(self) -> None:
2211 variables = dict(self.prepare_serverless_ide_environment())
2212 variables.update(self.setup_php_file("coverage.php"))
2213 self.test_driver.stop_hh_server()
2215 spec = (
2216 self.initialize_spec(
2217 LspTestSpec("serverless_ide_coverage"), use_serverless_ide=True
2219 .notification(
2220 method="textDocument/didOpen",
2221 params={
2222 "textDocument": {
2223 "uri": "${php_file_uri}",
2224 "languageId": "hack",
2225 "version": 1,
2226 "text": "${php_file}",
2230 .request(
2231 comment="Check type coverage",
2232 method="textDocument/typeCoverage",
2233 params={"textDocument": {"uri": "${php_file_uri}"}},
2234 result={
2235 "coveredPercent": 100,
2236 "uncoveredRanges": [],
2237 "defaultMessage": "Un-type checked code. Consider adding "
2238 "type annotations.",
2241 .request(comment="Shutdown", method="shutdown", params={}, result=None)
2243 self.run_spec(spec, variables, wait_for_server=False, use_serverless_ide=True)