Skip to content

Commit 7e31826

Browse files
committed
Allows for externals to be split across multiple lines
Fixes multiple declarations for external functions #169 Unittests have been added
1 parent 0b979cc commit 7e31826

File tree

7 files changed

+200
-5
lines changed

7 files changed

+200
-5
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
- Adds intrinsic support for `OpenACC` version 3.1
1111
- Adds sphinx autogenerated documentation
1212
- Adds `incl_suffixes` as a configuration option
13+
- Adds `EXTERNAL` as an attribute upon hover
1314

1415
### Changes
1516

@@ -21,6 +22,8 @@
2122
- Fixes the hover of preprocessor functions. It now displays the function name
2223
witout the argument list and the function body. The argument list cannot be
2324
multiline but the function body can.
25+
- Fixes objects marked `EXTERNAL` across multiple lines
26+
([#169](https://github.com/hansec/fortran-language-server/issues/169))
2427

2528
## 1.16.0
2629

fortls/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"elemental",
2727
"recursive",
2828
"abstract",
29+
"external",
2930
]
3031
KEYWORD_ID_DICT = {keyword: ind for (ind, keyword) in enumerate(KEYWORD_LIST)}
3132

fortls/objects.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1561,6 +1561,7 @@ def base_setup(
15611561
self.link_obj = None
15621562
self.type_obj = None
15631563
self.is_const: bool = False
1564+
self.is_external: bool = False
15641565
self.param_val: str = None
15651566
self.link_name: str = None
15661567
self.FQSN: str = self.name.lower()
@@ -1574,6 +1575,11 @@ def base_setup(
15741575
self.vis = -1
15751576
if self.keywords.count(KEYWORD_ID_DICT["parameter"]) > 0:
15761577
self.is_const = True
1578+
if (
1579+
self.keywords.count(KEYWORD_ID_DICT["external"]) > 0
1580+
or self.desc.lower() == "external"
1581+
):
1582+
self.is_external = True
15771583

15781584
def update_fqsn(self, enc_scope=None):
15791585
if enc_scope is not None:
@@ -1665,6 +1671,10 @@ def is_parameter(self):
16651671
def set_parameter_val(self, val: str):
16661672
self.param_val = val
16671673

1674+
def set_external_attr(self):
1675+
self.keywords.append(KEYWORD_ID_DICT["external"])
1676+
self.is_external = True
1677+
16681678
def check_definition(self, obj_tree, known_types={}, interface=False):
16691679
# Check for type definition in scope
16701680
type_match = DEF_KIND_REGEX.match(self.desc)
@@ -1871,6 +1881,7 @@ def __init__(self, file_obj=None):
18711881
self.parse_errors: list = []
18721882
self.inherit_objs: list = []
18731883
self.linkable_objs: list = []
1884+
self.external_objs: list = []
18741885
self.none_scope = None
18751886
self.inc_scope = None
18761887
self.current_scope = None
@@ -1945,12 +1956,14 @@ def end_scope(self, line_number: int, check: bool = True):
19451956
self.END_SCOPE_REGEX = None
19461957
self.enc_scope_name = self.get_enc_scope_name()
19471958

1948-
def add_variable(self, new_var):
1959+
def add_variable(self, new_var: fortran_var):
19491960
if self.current_scope is None:
19501961
self.create_none_scope()
19511962
new_var.FQSN = self.none_scope.FQSN + "::" + new_var.name.lower()
19521963
self.current_scope.add_child(new_var)
19531964
self.variable_list.append(new_var)
1965+
if new_var.is_external:
1966+
self.external_objs.append(new_var)
19541967
if new_var.require_link():
19551968
self.linkable_objs.append(new_var)
19561969
self.last_obj = new_var

fortls/parse_fortran.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,107 @@ def read_vis_stmnt(line: str):
664664
]
665665

666666

667+
def find_external_type(
668+
file_ast: fortran_ast, desc_string: str, name_stripped: str
669+
) -> bool:
670+
"""Encountered a variable with EXTERNAL as its type
671+
Try and find an already defined variable with a
672+
NORMAL Fortran Type"""
673+
if not desc_string.upper() == "EXTERNAL":
674+
return False
675+
counter = 0
676+
# Definition without EXTERNAL has already been parsed
677+
for v in file_ast.variable_list:
678+
if name_stripped == v.name:
679+
# If variable is already in external objs it has
680+
# been parsed correctly so exit
681+
if v in file_ast.external_objs:
682+
return False
683+
684+
v.set_external_attr()
685+
file_ast.external_objs.append(v)
686+
counter += 1
687+
# TODO: do I need to update AST any more?
688+
if counter == 1:
689+
return True
690+
else:
691+
return False
692+
693+
694+
def find_external_attr(
695+
file_ast: fortran_ast, name_stripped: str, new_var: fortran_var
696+
) -> bool:
697+
"""Check if this NORMAL Fortran variable is in the
698+
external_objs with only EXTERNAL as its type"""
699+
counter = 0
700+
for v in file_ast.external_objs:
701+
if v.name != name_stripped:
702+
continue
703+
if v.desc.upper() != "EXTERNAL":
704+
continue
705+
# We do this once
706+
if counter == 0:
707+
v.desc = new_var.desc
708+
v.set_external_attr()
709+
# TODO: do i need to update AST any more?
710+
counter += 1
711+
712+
# Only one definition encountered
713+
if counter == 1:
714+
return True
715+
# If no variable or multiple variables add to AST.
716+
# Multiple defs will throw diagnostic error as it should
717+
else:
718+
return False
719+
720+
721+
def find_external(
722+
file_ast: fortran_ast,
723+
desc_string: str,
724+
name_stripped: str,
725+
new_var: fortran_var,
726+
) -> bool:
727+
"""Find a procedure, function, subroutine, etc. that has been defined as
728+
`EXTERNAL`. `EXTERNAL`s are parsed as `fortran_var`, since there is no
729+
way of knowing if `real, external :: val` is a function or a subroutine.
730+
731+
This method exists solely for `EXTERNAL`s that are defined across multiple
732+
lines e.g.
733+
`EXTERNAL VAR`
734+
`REAL VAR`
735+
736+
or
737+
738+
`REAL VAR`
739+
`EXTERNAL VAR`
740+
741+
742+
Parameters
743+
----------
744+
file_ast : fortran_ast
745+
AST
746+
desc_string : str
747+
Variable type e.g. `REAL`, `INTEGER`, `EXTERNAL`
748+
name_stripped : str
749+
Variable name
750+
new_var : fortran_var
751+
The line variable that we are attempting to match with an `EXTERNAL`
752+
definition
753+
754+
Returns
755+
-------
756+
bool
757+
True if the variable is `EXTERNAL` and we manage to link it to the
758+
rest of its components, else False
759+
"""
760+
if find_external_type(file_ast, desc_string, name_stripped):
761+
return True
762+
elif desc_string.upper() != "EXTERNAL":
763+
if find_external_attr(file_ast, name_stripped, new_var):
764+
return True
765+
return False
766+
767+
667768
class fortran_file:
668769
def __init__(self, path: str = None, pp_suffixes: list = None):
669770
self.path: str = path
@@ -1607,6 +1708,12 @@ def parser_debug_msg(msg: str, line: str, ln: int):
16071708
if match:
16081709
var = match.group(1).strip()
16091710
new_var.set_parameter_val(var)
1711+
1712+
# Check if the "variable" is external and if so cycle
1713+
if find_external(file_ast, desc_string, name_stripped, new_var):
1714+
continue
1715+
1716+
# if not merge_external:
16101717
file_ast.add_variable(new_var)
16111718
parser_debug_msg("VARIABLE", line, line_number)
16121719

fortls/regex_patterns.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,15 @@
6363
NAT_VAR_REGEX = re.compile(
6464
r"[ ]*(INTEGER|REAL|DOUBLE[ ]*PRECISION|COMPLEX"
6565
r"|DOUBLE[ ]*COMPLEX|CHARACTER|LOGICAL|PROCEDURE"
66-
r"|EXTERNAL|CLASS|TYPE)",
66+
r"|EXTERNAL|CLASS|TYPE)", # external :: variable is handled by this
6767
re.I,
6868
)
6969
KIND_SPEC_REGEX = re.compile(r"[ ]*([*]?\([ ]*[a-z0-9_*:]|\*[ ]*[0-9:]*)", re.I)
7070
KEYWORD_LIST_REGEX = re.compile(
7171
r"[ ]*,[ ]*(PUBLIC|PRIVATE|ALLOCATABLE|"
7272
r"POINTER|TARGET|DIMENSION\(|"
7373
r"OPTIONAL|INTENT\([inout]*\)|DEFERRED|NOPASS|"
74-
r"PASS\([a-z0-9_]*\)|SAVE|PARAMETER|"
74+
r"PASS\([a-z0-9_]*\)|SAVE|PARAMETER|EXTERNAL|"
7575
r"CONTIGUOUS)",
7676
re.I,
7777
)

test/test_server.py

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ def check_return(result_array):
175175
objs = (
176176
["test", 6, 7],
177177
["test_abstract", 2, 0],
178+
["test_external", 2, 0],
178179
["test_free", 2, 0],
179180
["test_gen_type", 5, 1],
180181
["test_generic", 2, 0],
@@ -586,6 +587,11 @@ def test_diagnostics():
586587
"""
587588
Tests some aspects of diagnostics
588589
"""
590+
591+
def check_return(results, ref_results):
592+
for i, r in enumerate(results):
593+
assert r["diagnostics"] == ref_results[i]
594+
589595
string = write_rpc_request(1, "initialize", {"rootPath": test_dir})
590596
# Test subroutines and functions with interfaces as arguments
591597
file_path = os.path.join(test_dir, "test_diagnostic_int.f90")
@@ -607,10 +613,64 @@ def test_diagnostics():
607613
string += write_rpc_notification(
608614
"textDocument/didOpen", {"textDocument": {"uri": file_path}}
609615
)
616+
# Test that externals can be split between multiple lines
617+
# and that diagnostics for multiple definitions of externals can account
618+
# for that
619+
file_path = os.path.join(test_dir, "diag", "test_external.f90")
620+
string += write_rpc_notification(
621+
"textDocument/didOpen", {"textDocument": {"uri": file_path}}
622+
)
610623
errcode, results = run_request(string)
611624
assert errcode == 0
612-
# check that the diagnostics list is empty
613-
assert not results[1]["diagnostics"]
625+
ref_results = [
626+
[],
627+
[],
628+
[],
629+
[],
630+
[
631+
{
632+
"range": {
633+
"start": {"line": 7, "character": 17},
634+
"end": {"line": 7, "character": 22},
635+
},
636+
"message": 'Variable "VAR_B" declared twice in scope',
637+
"severity": 1,
638+
"relatedInformation": [
639+
{
640+
"location": {
641+
"uri": f"file://{file_path}",
642+
"range": {
643+
"start": {"line": 5, "character": 0},
644+
"end": {"line": 5, "character": 0},
645+
},
646+
},
647+
"message": "First declaration",
648+
}
649+
],
650+
},
651+
{
652+
"range": {
653+
"start": {"line": 8, "character": 17},
654+
"end": {"line": 8, "character": 22},
655+
},
656+
"message": 'Variable "VAR_A" declared twice in scope',
657+
"severity": 1,
658+
"relatedInformation": [
659+
{
660+
"location": {
661+
"uri": f"file://{file_path}",
662+
"range": {
663+
"start": {"line": 3, "character": 0},
664+
"end": {"line": 3, "character": 0},
665+
},
666+
},
667+
"message": "First declaration",
668+
}
669+
],
670+
},
671+
],
672+
]
673+
check_return(results[1:], ref_results)
614674

615675

616676
if __name__ == "__main__":
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
program test_external
2+
implicit none
3+
REAL, EXTERNAL :: VAL
4+
REAL VAR_A
5+
EXTERNAL VAR_A
6+
EXTERNAL VAR_B
7+
REAL VAR_B
8+
EXTERNAL VAR_B ! throw error
9+
REAL VAR_A ! throw error
10+
EXTERNAL VAR_C
11+
end program test_external

0 commit comments

Comments
 (0)