Skip to content

Commit 20f2e3d

Browse files
committed
Fixes function modifiers not displaying on hover
It required major rewrite of the function definition parsing Closes Functions with multiple keywords display incorrect hover #48 TODO: 1. Add tests about intrinsic functions 2. Add tests about functions returning arrays (this is actually a big deal) 2. touches on if hover should be displaying FORTRAN abiding code. I am leaning towards yes, so displaying ```fortran real dimension(10,10) function foo(arg) result(val) real, intent(in) :: arg ``` Does not seem the right thing for me to do. I think the hover request should return instead ```fortran real function foo(arg) result(val) real, intent(in) :: arg real, dimension(10,10) :: val ``` This way syntax highlighting in VSCode will not complain
1 parent 0f87e6a commit 20f2e3d

File tree

7 files changed

+85
-36
lines changed

7 files changed

+85
-36
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
([gnikit/fortls#22](https://github.com/gnikit/fortls/issues/22))
99
- Fixed function hovering signature now standardised
1010
([#47](https://github.com/hansec/fortran-language-server/issues/47))
11+
- Fixed function modifiers not displaying upon hover
12+
([gnikit/fortls#48](https://github.com/gnikit/fortls/issues/48))
1113

1214
## 2.2.1
1315

fortls/constants.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
from __future__ import annotations
2+
13
import logging
24
import sys
5+
from dataclasses import dataclass, field
36

47
PY3K = sys.version_info >= (3, 0)
58

@@ -58,3 +61,23 @@
5861
# it cannot also be a comment that requires !, c, d
5962
# and ^= (xor_eq) operator is invalid in Fortran C++ preproc
6063
FORTRAN_LITERAL = "0^=__LITERAL_INTERNAL_DUMMY_VAR_"
64+
65+
66+
@dataclass
67+
class RESULT_sig:
68+
name: str = field(default=None)
69+
type: str = field(default=None)
70+
keywords: list[str] = field(default_factory=list)
71+
72+
73+
@dataclass
74+
class FUN_sig:
75+
name: str
76+
args: str
77+
keywords: list[str] = field(default_factory=list)
78+
mod_flag: bool = field(default=False)
79+
result: RESULT_sig = field(default_factory=RESULT_sig)
80+
81+
def __post_init__(self):
82+
if not self.result.name:
83+
self.result.name = self.name

fortls/intrinsics.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,9 @@ def create_object(json_obj, enc_obj=None):
117117
0,
118118
name,
119119
args=args,
120-
result_type=[json_obj["return"], keywords, keyword_info],
120+
result_type=json_obj["return"],
121+
keywords=keywords,
122+
# keyword_info=keyword_info,
121123
)
122124
elif json_obj["type"] == 3:
123125
return fortran_var(

fortls/objects.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1087,7 +1087,7 @@ def __init__(
10871087
args: str = "",
10881088
mod_flag: bool = False,
10891089
keywords: list = None,
1090-
result_type: list[str] = None, # TODO: make this a string
1090+
result_type: str = None,
10911091
result_name: str = None,
10921092
):
10931093
super().__init__(file_ast, line_number, name, args, mod_flag, keywords)
@@ -1098,10 +1098,8 @@ def __init__(
10981098
self.missing_args: list = []
10991099
self.mod_scope: bool = mod_flag
11001100
self.result_name: str = result_name
1101-
self.result_type: str = None
1101+
self.result_type: str = result_type
11021102
self.result_obj: fortran_var = None
1103-
if result_type:
1104-
self.result_type = result_type[0]
11051103
# Set the implicit result() name to be the function name
11061104
if self.result_name is None:
11071105
self.result_name = self.name

fortls/parse_fortran.py

Lines changed: 38 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
import os
66
import re
77
import sys
8+
from typing import Literal
89

910
from fortls.constants import (
1011
DO_TYPE_ID,
1112
INTERFACE_TYPE_ID,
1213
PY3K,
1314
SELECT_TYPE_ID,
1415
SUBMODULE_TYPE_ID,
16+
FUN_sig,
17+
RESULT_sig,
1518
log,
1619
)
1720
from fortls.helper_functions import (
@@ -27,7 +30,6 @@
2730
)
2831
from fortls.objects import (
2932
CLASS_info,
30-
FUN_info,
3133
GEN_info,
3234
INT_info,
3335
SELECT_info,
@@ -266,8 +268,8 @@ def read_var_def(line: str, type_word: str = None, fun_only: bool = False):
266268
return None
267269
#
268270
keywords, trailing_line = parse_var_keywords(trailing_line)
269-
# Check if function
270-
fun_def = read_fun_def(trailing_line, [type_word, keywords])
271+
# Check if this is a function definition
272+
fun_def = read_fun_def(trailing_line, RESULT_sig(type=type_word, keywords=keywords))
271273
if (fun_def is not None) or fun_only:
272274
return fun_def
273275
#
@@ -287,34 +289,42 @@ def read_var_def(line: str, type_word: str = None, fun_only: bool = False):
287289
return "var", VAR_info(type_word, keywords, var_words)
288290

289291

290-
def read_fun_def(line: str, result_type=None, mod_flag: bool = False):
292+
def read_fun_def(
293+
line: str, result: RESULT_sig = RESULT_sig(), mod_flag: bool = False
294+
) -> tuple[Literal["fun"], FUN_sig] | None:
291295
"""Attempt to read FUNCTION definition line
292296
297+
To infer the `result` `type` and `name` the variable definition is called
298+
with the function only flag
299+
293300
Parameters
294301
----------
295302
line : str
296303
file line
297-
result_type : str, optional
298-
type of function e.g. INTEGER, REAL, etc., by default None
304+
result : RESULT_sig, optional
305+
a dataclass containing the result signature of the function
299306
mod_flag : bool, optional
300307
flag for module and module procedure parsing, by default False
301308
302309
Returns
303310
-------
304-
tuple[Literal['fun'], FUN_info]
311+
tuple[Literal["fun"], FUN_sig] | None
305312
a named tuple
306313
"""
307-
mod_match = SUB_MOD_REGEX.match(line)
308-
mods_found = False
309-
keywords: list[str] = []
310-
while mod_match is not None:
311-
mods_found = True
312-
line = line[mod_match.end(0) :]
313-
keywords.append(mod_match.group(1))
314-
mod_match = SUB_MOD_REGEX.match(line)
315-
if mods_found:
314+
# Get all the keyword modifier mathces
315+
keywords = re.findall(SUB_MOD_REGEX, line)
316+
# remove modifiers from line
317+
for modifier in keywords:
318+
line = line.replace(modifier, "")
319+
320+
# Try and get the result type
321+
# Recursively will call read_var_def which will then call read_fun_def
322+
# with the variable result having been populated
323+
if keywords:
316324
tmp_var = read_var_def(line, fun_only=True)
317325
if tmp_var is not None:
326+
# Update keywords for function into dataclass
327+
tmp_var[1].keywords = keywords
318328
return tmp_var
319329
fun_match = FUN_REGEX.match(line)
320330
if fun_match is None:
@@ -334,12 +344,11 @@ def read_fun_def(line: str, result_type=None, mod_flag: bool = False):
334344
trailing_line = trailing_line[paren_match.end(0) :]
335345

336346
# Extract if possible the variable name of the result()
337-
result_name = None
338347
trailing_line = trailing_line.strip()
339348
results_match = RESULT_REGEX.match(trailing_line)
340349
if results_match:
341-
result_name = results_match.group(1).strip().lower()
342-
return "fun", FUN_info(name, args, result_type, result_name, mod_flag, keywords)
350+
result.name = results_match.group(1).strip().lower()
351+
return "fun", FUN_sig(name, args, keywords, mod_flag, result=result)
343352

344353

345354
def read_sub_def(line: str, mod_flag: bool = False):
@@ -548,7 +557,8 @@ def read_mod_def(line: str):
548557
return sub_res
549558
fun_res = read_var_def(trailing_line, fun_only=True)
550559
if fun_res is not None:
551-
return fun_res[0], fun_res[1]._replace(mod_flag=True)
560+
fun_res[1].mod_flag = True
561+
return fun_res[0], fun_res[1]
552562
fun_res = read_fun_def(trailing_line, mod_flag=True)
553563
if fun_res is not None:
554564
return fun_res
@@ -1787,24 +1797,21 @@ def parser_debug_msg(msg: str, line: str, ln: int):
17871797
args=obj_info.args,
17881798
mod_flag=obj_info.mod_flag,
17891799
keywords=keywords,
1790-
result_type=obj_info.return_type,
1791-
result_name=obj_info.return_var,
1800+
result_type=obj_info.result.type,
1801+
result_name=obj_info.result.name,
17921802
)
17931803
file_ast.add_scope(new_fun, END_FUN_REGEX)
17941804
# function type is present without result(), register the automatic
17951805
# result() variable that is the function name
1796-
if obj_info.return_type:
1797-
result_name = obj_info.name
1798-
if obj_info.return_var:
1799-
result_name = obj_info.return_var
1800-
keywords, keyword_info = map_keywords(obj_info.return_type[1])
1806+
if obj_info.result.type:
1807+
keywords, keyword_info = map_keywords(obj_info.result.keywords)
18011808
new_obj = fortran_var(
18021809
file_ast,
18031810
line_number,
1804-
result_name,
1805-
obj_info.return_type[0],
1806-
keywords,
1807-
keyword_info,
1811+
name=obj_info.result.name,
1812+
var_desc=obj_info.result.type,
1813+
keywords=keywords,
1814+
keyword_info=keyword_info,
18081815
)
18091816
file_ast.add_variable(new_obj)
18101817
parser_debug_msg("FUNCTION", line, line_number)

test/test_server.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,8 +578,10 @@ def check_return(result_array, checks):
578578
string += hover_req(file_path, 7, 19)
579579
string += hover_req(file_path, 12, 12)
580580
string += hover_req(file_path, 18, 19)
581+
string += hover_req(file_path, 23, 34)
581582
file_path = test_dir / "subdir" / "test_submod.F90"
582583
string += hover_req(file_path, 29, 24)
584+
string += hover_req(file_path, 34, 24)
583585
file_path = test_dir / "test_diagnostic_int.f90"
584586
string += hover_req(file_path, 19, 14)
585587

@@ -608,8 +610,17 @@ def check_return(result_array, checks):
608610
INTEGER, INTENT(IN) :: arg""",
609611
"""INTEGER FUNCTION fun4(arg) RESULT(retval)
610612
INTEGER, INTENT(IN) :: arg""",
613+
# Notice that the order of the modifiers does not match the source code
614+
# This is part of the test, ideally they would be identical but previously
615+
# any modifiers before the type would be discarded
616+
"""INTEGER PURE ELEMENTAL FUNCTION fun5(arg) RESULT(retval)
617+
INTEGER, INTENT(IN) :: arg""",
618+
# TODO: more tests to add from functions
611619
"""REAL FUNCTION point_dist(a, b) RESULT(distance)
612620
TYPE(point), INTENT(IN) :: a
621+
TYPE(point), INTENT(IN) :: b""",
622+
"""LOGICAL FUNCTION is_point_equal_a(a, b) RESULT(is_point_equal_a)
623+
TYPE(point), INTENT(IN) :: a
613624
TYPE(point), INTENT(IN) :: b""",
614625
"""REAL FUNCTION foo2(f, g, h) RESULT(arg3)
615626
REAL FUNCTION f(x) RESULT(z) :: f

test/test_source/hover/functions.f90

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,9 @@ end function fun3
1919
integer function fun4(arg) result(retval)
2020
integer, intent(in) :: arg
2121
end function fun4
22+
23+
! function with type on definition, return and keywords
24+
pure integer elemental function fun5(arg) result(retval)
25+
integer, intent(in) :: arg
26+
end function fun5
27+

0 commit comments

Comments
 (0)