diff --git a/CHANGELOG.md b/CHANGELOG.md index c234d0a4..f1131bef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ - Fixed missing registered capability for `textDocument/documentHighlight` ([#421](https://github.com/fortran-lang/fortls/issues/421s)) +- Fixed `textDocument/rename` not renaming the implicit result variable of a + function declared without a `RESULT()` clause + ([#322](https://github.com/fortran-lang/fortls/issues/322)) + ## 3.2.2 ### Fixed diff --git a/fortls/langserver.py b/fortls/langserver.py index b53850d6..4ae4192e 100644 --- a/fortls/langserver.py +++ b/fortls/langserver.py @@ -969,6 +969,27 @@ def get_all_references( # A container that includes all the FQSN signatures for objects that # are linked to the rename request and that should also be replaced override_cache: list[str] = [] + # A function without an explicit RESULT() clause returns through an + # implicit result variable that shares the function's name but is a + # distinct object with its own FQSN, e.g. `mod::fun` vs + # `mod::fun::fun`. The two must always be renamed together or the code + # stops compiling, so link them in both directions. + result_obj = getattr(def_obj, "result_obj", None) + if ( + def_obj.get_type() == FUNCTION_TYPE_ID + and result_obj is not None + and def_obj.result_name.lower() == def_name + ): + override_cache.append(result_obj.FQSN) + else: + parent = getattr(def_obj, "parent", None) + if ( + parent is not None + and parent.get_type() == FUNCTION_TYPE_ID + and getattr(parent, "result_obj", None) is def_obj + and parent.result_name.lower() == parent.name.lower() + ): + override_cache.append(parent.FQSN) refs = {} ref_objs = [] for filename, file_obj in file_set: diff --git a/test/test_server_rename.py b/test/test_server_rename.py index 3c67d1a2..013b891d 100644 --- a/test/test_server_rename.py +++ b/test/test_server_rename.py @@ -199,3 +199,63 @@ def test_rename_skip_intrinsic(): errcode, results = run_request(string, ["-n", "1"]) # FIXME: to be implemented assert errcode == 0 + + +def test_rename_implicit_function_result(): + """Test that renaming a function without a RESULT() clause also renames the + implicit result variable in the function body, see issue #322. + """ + string = write_rpc_request(1, "initialize", {"rootPath": str(test_dir / "rename")}) + file_path = test_dir / "rename" / "test_rename_implicit_result.f90" + string += rename_request("sin_deg", file_path, 7, 25) + errcode, results = run_request(string, ["-n", "1"]) + assert errcode == 0 + ref = {} + ref[path_to_uri(str(file_path))] = [ + create("sin_deg", 7, 23, 7, 27), + create("sin_deg", 9, 8, 9, 12), + ] + # zip() in check_rename_response silently ignores missing changes, + # so assert the count explicitly + assert len(results[1]["changes"][path_to_uri(str(file_path))]) == 2 + check_rename_response(results[1]["changes"], ref) + + +def test_rename_implicit_function_result_from_body(): + """Test that renaming from the implicit result variable in the body also + renames the function definition, see issue #322. + """ + string = write_rpc_request(1, "initialize", {"rootPath": str(test_dir / "rename")}) + file_path = test_dir / "rename" / "test_rename_implicit_result.f90" + string += rename_request("sin_deg", file_path, 9, 9) + errcode, results = run_request(string, ["-n", "1"]) + assert errcode == 0 + ref = {} + ref[path_to_uri(str(file_path))] = [ + create("sin_deg", 7, 23, 7, 27), + create("sin_deg", 9, 8, 9, 12), + ] + # zip() in check_rename_response silently ignores missing changes, + # so assert the count explicitly + assert len(results[1]["changes"][path_to_uri(str(file_path))]) == 2 + check_rename_response(results[1]["changes"], ref) + + +def test_rename_explicit_function_result(): + """Test that a function with an explicit RESULT() clause only renames the + result variable, not the function name, see issue #322. + """ + string = write_rpc_request(1, "initialize", {"rootPath": str(test_dir / "rename")}) + file_path = test_dir / "rename" / "test_rename_implicit_result.f90" + string += rename_request("res", file_path, 16, 9) + errcode, results = run_request(string, ["-n", "1"]) + assert errcode == 0 + ref = {} + ref[path_to_uri(str(file_path))] = [ + create("res", 14, 39, 14, 40), + create("res", 16, 8, 16, 9), + ] + # zip() in check_rename_response silently ignores missing changes, + # so assert the count explicitly + assert len(results[1]["changes"][path_to_uri(str(file_path))]) == 2 + check_rename_response(results[1]["changes"], ref) diff --git a/test/test_source/rename/test_rename_implicit_result.f90 b/test/test_source/rename/test_rename_implicit_result.f90 new file mode 100644 index 00000000..8a6056aa --- /dev/null +++ b/test/test_source/rename/test_rename_implicit_result.f90 @@ -0,0 +1,20 @@ +module test_rename_implicit_result + implicit none +contains + + ! No RESULT() clause: the function returns through an implicit result + ! variable that shares the function's name. Renaming the function must + ! rename the assignment below too. See issue #322. + real pure function sind(x) + real, intent(in), value :: x + sind = sin(x*((4.0*atan(1.0))/180.0)) + end function + + ! Explicit RESULT() clause: the function name is not a variable in the + ! body, so renaming the function must NOT touch `r`. + real function withresult(x) result(r) + real, intent(in) :: x + r = x*3.0 + end function + +end module test_rename_implicit_result