Skip to content

Commit 8b01495

Browse files
committed
Improves renaming functionality
- Added unittests for renaming - Properly implemented solution for hansec/fortran-language-server#104 Now it does not add to the source code. - Improves coverage of renaming and all_references - Added warnings for edge cases i.e. renaming intrinsics
1 parent 364b9c4 commit 8b01495

File tree

9 files changed

+322
-44
lines changed

9 files changed

+322
-44
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# CHANGELONG
22

3+
## 2.2.8
4+
5+
### Changed
6+
7+
- Changed how renaming of implicitly named type-bound procedures and their
8+
implementations is handled. Unittest was added.
9+
- Rewrote the Fortran parser to be clearer and more modular
10+
311
## 2.2.7
412

513
### Changed

fortls/langserver.py

Lines changed: 53 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
set_keyword_ordering,
4242
)
4343
from fortls.intrinsics import (
44+
fortran_intrinsic_obj,
4445
get_intrinsic_keywords,
4546
load_intrinsics,
4647
set_lowercase_intrinsics,
@@ -905,6 +906,8 @@ def get_all_references(
905906
file_set = self.workspace.items()
906907
else:
907908
file_set = ((file_obj.path, file_obj),)
909+
# A container that includes all the FQSN signatures for objects that
910+
# are linked to the rename request and that should also be replaced
908911
override_cache: list[str] = []
909912
refs = {}
910913
ref_objs = []
@@ -923,26 +926,52 @@ def get_all_references(
923926
if var_def is None:
924927
continue
925928
ref_match = False
926-
if def_fqsn == var_def.FQSN or var_def.FQSN in override_cache:
927-
ref_match = True
928-
elif var_def.parent and var_def.parent.get_type() == CLASS_TYPE_ID:
929-
if type_mem:
930-
for inherit_def in var_def.parent.get_overridden(def_name):
931-
if def_fqsn == inherit_def.FQSN:
932-
ref_match = True
933-
override_cache.append(var_def.FQSN)
934-
break
935-
if (
936-
(var_def.sline - 1 == i)
937-
and (var_def.file_ast.path == filename)
938-
and (line.count("=>") == 0)
929+
try:
930+
# NOTE: throws AttributeError if object is intrinsic since
931+
# it will not have a FQSN
932+
# BUG: intrinsic objects should be excluded, but get_definition
933+
# does not recognise the arguments
934+
if def_fqsn == var_def.FQSN or var_def.FQSN in override_cache:
935+
ref_match = True
936+
# NOTE: throws AttributeError if object is None
937+
elif var_def.parent.get_type() == CLASS_TYPE_ID:
938+
if type_mem:
939+
for inherit_def in var_def.parent.get_overridden(
940+
def_name
941+
):
942+
if def_fqsn == inherit_def.FQSN:
943+
ref_match = True
944+
override_cache.append(var_def.FQSN)
945+
break
946+
947+
# Standalone definition of a type-bound procedure,
948+
# no pointer replace all its instances in the current scope
949+
# NOTE: throws AttributeError if object has no link_obj
950+
if (
951+
var_def.sline - 1 == i
952+
and var_def.file_ast.path == filename
953+
and line.count("=>") == 0
954+
and var_def.link_obj is def_obj
955+
):
956+
ref_objs.append(var_def)
957+
override_cache.append(var_def.FQSN)
958+
ref_match = True
959+
960+
# Object is a Method and the linked object i.e. the
961+
# implementation
962+
# shares the same parent signature as the current variable
963+
# NOTE:: throws and AttributeError if the link_object or
964+
# parent are not present OR they are set to None
965+
# hence not having a FQSN
966+
elif (
967+
def_obj.get_type(True) == METH_TYPE_ID
968+
and def_obj.link_obj.parent.FQSN == var_def.parent.FQSN
939969
):
940-
try:
941-
if var_def.link_obj is def_obj:
942-
ref_objs.append(var_def)
943-
ref_match = True
944-
except:
945-
pass
970+
ref_match = True
971+
override_cache.append(var_def.FQSN)
972+
except AttributeError:
973+
ref_match = False
974+
946975
if ref_match:
947976
file_refs.append([i, match.start(1), match.end(1)])
948977
if len(file_refs) > 0:
@@ -1114,6 +1143,9 @@ def serve_rename(self, request: dict):
11141143
def_obj = self.get_definition(file_obj, def_line, def_char)
11151144
if def_obj is None:
11161145
return None
1146+
if isinstance(def_obj, fortran_intrinsic_obj):
1147+
self.post_message("Rename failed: Cannot rename intrinsics", Severity.warn)
1148+
return None
11171149
# Determine global accesibility and type membership
11181150
restrict_file = None
11191151
type_mem = False
@@ -1132,36 +1164,14 @@ def serve_rename(self, request: dict):
11321164
return None
11331165
# Create rename changes
11341166
new_name = params["newName"]
1135-
changes = {}
1136-
for (filename, file_refs) in all_refs.items():
1167+
changes: dict[str, list[dict]] = {}
1168+
for filename, file_refs in all_refs.items():
11371169
file_uri = path_to_uri(filename)
11381170
changes[file_uri] = []
11391171
for ref in file_refs:
11401172
changes[file_uri].append(
11411173
change_json(new_name, ref[0], ref[1], ref[0], ref[2])
11421174
)
1143-
# Check for implicit procedure implementation naming
1144-
bind_obj = None
1145-
if def_obj.get_type(no_link=True) == METH_TYPE_ID:
1146-
_, curr_line, post_lines = def_obj.file_ast.file.get_code_line(
1147-
def_obj.sline - 1, backward=False, strip_comment=True
1148-
)
1149-
if curr_line is not None:
1150-
full_line = curr_line + "".join(post_lines)
1151-
if full_line.find("=>") < 0:
1152-
bind_obj = def_obj
1153-
bind_change = f"{new_name} => {def_obj.name}"
1154-
elif (len(ref_objs) > 0) and (
1155-
ref_objs[0].get_type(no_link=True) == METH_TYPE_ID
1156-
):
1157-
bind_obj = ref_objs[0]
1158-
bind_change = f"{ref_objs[0].name} => {new_name}"
1159-
# Replace definition statement with explicit implementation naming
1160-
if bind_obj is not None:
1161-
def_uri = path_to_uri(bind_obj.file_ast.file.path)
1162-
for change in changes[def_uri]:
1163-
if change["range"]["start"]["line"] == bind_obj.sline - 1:
1164-
change["newText"] = bind_change
11651175
return {"changes": changes}
11661176

11671177
def serve_codeActions(self, request: dict):

test/setup_tests.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818
test_dir = root_dir / "test" / "test_source"
1919

2020

21+
def check_post_msg(result: dict, msg: str, severity: int):
22+
assert result["type"] == severity
23+
assert result["message"] == msg
24+
25+
2126
def run_request(request, fortls_args: list[str] = None):
2227
command = [
2328
sys.executable,

test/test_server_rename.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
from setup_tests import (
2+
check_post_msg,
3+
path_to_uri,
4+
run_request,
5+
test_dir,
6+
write_rpc_request,
7+
)
8+
9+
10+
def rename_request(new_name: str, file_path, ln: int, ch: int):
11+
return write_rpc_request(
12+
1,
13+
"textDocument/rename",
14+
{
15+
"newName": new_name,
16+
"textDocument": {"uri": str(file_path)},
17+
"position": {"line": ln, "character": ch},
18+
},
19+
)
20+
21+
22+
def check_rename_response(response: dict, references: dict):
23+
# Loop over URI's if the change spans multiple files there will be more than 1
24+
for uri, changes in response.items():
25+
refs = references[uri]
26+
# Loop over all the changes in the current URI, instances of object
27+
for c, r in zip(changes, refs):
28+
assert c["range"] == r["range"]
29+
assert c["newText"] == r["newText"]
30+
31+
32+
def create(new_text: str, sln: int, sch: int, eln: int, ech: int):
33+
return {
34+
"range": {
35+
"start": {"line": sln, "character": sch},
36+
"end": {"line": eln, "character": ech},
37+
},
38+
"newText": new_text,
39+
}
40+
41+
42+
def test_rename_var():
43+
""" "Test simple variable rename"""
44+
string = write_rpc_request(1, "initialize", {"rootPath": str(test_dir)})
45+
file_path = test_dir / "test_prog.f08"
46+
string += rename_request("str_rename", file_path, 5, 25)
47+
errcode, results = run_request(string)
48+
assert errcode == 0
49+
ref = {}
50+
ref[path_to_uri(str(file_path))] = [create("str_rename", 5, 20, 5, 29)]
51+
check_rename_response(results[1]["changes"], ref)
52+
53+
54+
def test_rename_var_across_module():
55+
"""Test renaming objects like variables across modules works"""
56+
string = write_rpc_request(1, "initialize", {"rootPath": str(test_dir)})
57+
file_path = test_dir / "test_prog.f08"
58+
string += rename_request("new_module_var", file_path, 26, 15)
59+
errcode, results = run_request(string)
60+
assert errcode == 0
61+
ref = {}
62+
ref[path_to_uri(str(test_dir / "subdir" / "test_free.f90"))] = [
63+
create("new_module_var", 32, 11, 32, 26)
64+
]
65+
ref[path_to_uri(str(file_path))] = [create("new_module_var", 2, 44, 2, 59)]
66+
ref[path_to_uri(str(file_path))].append(create("new_module_var", 26, 8, 26, 23))
67+
68+
check_rename_response(results[1]["changes"], ref)
69+
70+
71+
def test_rename_empty():
72+
"""Test that renaming nothing will not error"""
73+
string = write_rpc_request(1, "initialize", {"rootPath": str(test_dir / "rename")})
74+
file_path = test_dir / "rename" / "test_rename_imp_type_bound_proc.f90"
75+
string += rename_request("bar", file_path, 9, 0)
76+
errcode, results = run_request(string, ["-n", "1"])
77+
assert errcode == 0
78+
assert results[1] is None
79+
80+
81+
def test_rename_member_type_ptr():
82+
"""Test that renaming type bound pointers of procedure methods rename
83+
only the pointer and not the implementation, even if the pointer and the
84+
implementation share the same name
85+
"""
86+
string = write_rpc_request(1, "initialize", {"rootPath": str(test_dir)})
87+
file_path = test_dir / "test_prog.f08"
88+
string += rename_request("bp_rename", file_path, 18, 25)
89+
errcode, results = run_request(string)
90+
assert errcode == 0
91+
ref = {}
92+
ref[path_to_uri(str(file_path))] = [create("bp_rename", 18, 16, 18, 26)]
93+
ref[path_to_uri(str(test_dir / "subdir" / "test_free.f90"))] = [
94+
create("bp_rename", 15, 27, 15, 37)
95+
]
96+
check_rename_response(results[1]["changes"], ref)
97+
98+
99+
def test_rename_member_type_ptr_null():
100+
"""Test renaming type bound pointers of procedure methods works when pointing to
101+
null
102+
"""
103+
string = write_rpc_request(1, "initialize", {"rootPath": str(test_dir)})
104+
file_path = test_dir / "test_prog.f08"
105+
string += rename_request("bp_rename", file_path, 17, 25)
106+
errcode, results = run_request(string)
107+
assert errcode == 0
108+
ref = {}
109+
ref[path_to_uri(str(file_path))] = [create("bp_rename", 17, 16, 17, 28)]
110+
ref[path_to_uri(str(test_dir / "subdir" / "test_free.f90"))] = [
111+
create("bp_rename", 11, 43, 11, 55)
112+
]
113+
check_rename_response(results[1]["changes"], ref)
114+
115+
116+
def test_rename_type_bound_proc_no_ptr():
117+
"""Test renaming type bound pointers of procedure methods works when no pointer
118+
is setup. Requesting to rename the procedure should rename, the implementation
119+
and the Method itself i.e. call self%foo()
120+
121+
Requesting to rename the implementation should also rename the procedure
122+
and all the locations it is called in
123+
"""
124+
root = test_dir / "rename"
125+
string = write_rpc_request(1, "initialize", {"rootPath": str(root)})
126+
file_path = root / "test_rename_imp_type_bound_proc.f90"
127+
# Rename the procedure name and check if implementation also renames
128+
string += rename_request("bar", file_path, 5, 23)
129+
# Rename the implementation name and check if declaration, references change
130+
string += rename_request("bar", file_path, 10, 18)
131+
errcode, results = run_request(string)
132+
assert errcode == 0
133+
ref = {}
134+
ref[path_to_uri(str(file_path))] = [create("bar", 5, 21, 5, 24)]
135+
ref[path_to_uri(str(file_path))].append(create("bar", 10, 15, 10, 18))
136+
ref[path_to_uri(str(file_path))].append(create("bar", 12, 18, 12, 21))
137+
ref[path_to_uri(str(file_path))].append(create("bar", 13, 19, 13, 22))
138+
check_rename_response(results[1]["changes"], ref)
139+
check_rename_response(results[2]["changes"], ref)
140+
141+
142+
def test_rename_non_existent_file():
143+
"""Test renaming type bound pointers of procedure methods works when pointing to
144+
null
145+
"""
146+
string = write_rpc_request(1, "initialize", {"rootPath": str(test_dir)})
147+
file_path = test_dir / "fake.f90"
148+
string += rename_request("bar", file_path, 5, 23)
149+
errcode, results = run_request(string)
150+
assert errcode == 0
151+
assert results[1] is None
152+
153+
154+
def test_rename_nested():
155+
"""Test renaming heavily nested constructs works"""
156+
string = write_rpc_request(1, "initialize", {"rootPath": str(test_dir / "rename")})
157+
file_path = test_dir / "rename" / "test_rename_nested.f90"
158+
string += rename_request("bar", file_path, 6, 23)
159+
errcode, results = run_request(string, ["-n", "1"])
160+
assert errcode == 0
161+
ref = {}
162+
ref[path_to_uri(str(file_path))] = [create("bar", 6, 23, 6, 26)]
163+
ref[path_to_uri(str(file_path))].append(create("bar", 9, 27, 9, 30))
164+
check_rename_response(results[1]["changes"], ref)
165+
166+
167+
def test_rename_intrinsic():
168+
"""Test renaming an intrinsic function, while no other function exists
169+
with the same name, will throw an error
170+
"""
171+
string = write_rpc_request(1, "initialize", {"rootPath": str(test_dir / "rename")})
172+
file_path = test_dir / "rename" / "test_rename_nested.f90"
173+
string += rename_request("bar", file_path, 8, 27)
174+
errcode, results = run_request(string, ["-n", "1"])
175+
assert errcode == 0
176+
check_post_msg(results[1], "Rename failed: Cannot rename intrinsics", 2)
177+
assert results[2] is None
178+
179+
180+
def test_rename_use_only_rename():
181+
"""Test renaming constructs of `use mod, only: val => root_val
182+
are handled correctly
183+
"""
184+
string = write_rpc_request(1, "initialize", {"rootPath": str(test_dir / "subdir")})
185+
file_path = test_dir / "subdir" / "test_rename.F90"
186+
string += rename_request("bar", file_path, 13, 5)
187+
errcode, results = run_request(string, ["-n", "1"])
188+
# FIXME: to be implemented
189+
assert errcode == 0
190+
191+
192+
def test_rename_skip_intrinsic():
193+
"""Test that renaming functions named the same as intrinsic functions e.g. size()
194+
will only rename the user defined functions
195+
"""
196+
string = write_rpc_request(1, "initialize", {"rootPath": str(test_dir / "rename")})
197+
file_path = test_dir / "rename" / "test_rename_intrinsic.f90"
198+
string += rename_request("bar", file_path, 22, 13)
199+
errcode, results = run_request(string, ["-n", "1"])
200+
# FIXME: to be implemented
201+
assert errcode == 0

test/test_source/.fortls

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"excl_paths": [
66
"excldir/**",
77
"./diag/",
8-
"docs"
8+
"docs",
9+
"rename"
910
]
1011

1112
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module mod
2+
implicit none
3+
4+
type :: t
5+
contains
6+
procedure :: foo
7+
end type t
8+
9+
contains
10+
11+
subroutine foo(self)
12+
class(t), intent(in) :: self
13+
call self%foo()
14+
end subroutine foo
15+
end module mod

0 commit comments

Comments
 (0)