Skip to content

Commit

Permalink
Merge pull request #130 from gnikit/feature/kind-improvements
Browse files Browse the repository at this point in the history
Feature/kind-improvements
  • Loading branch information
gnikit authored May 29, 2022
2 parents 608d94c + 3c098c4 commit 573dc60
Show file tree
Hide file tree
Showing 10 changed files with 288 additions and 51 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
run: pip install .[dev]

- name: Unittests
run: pytest
run: pytest --doctest-modules

- name: Lint
run: black --diff --check --verbose .
Expand All @@ -45,7 +45,7 @@ jobs:
- name: Coverage report
run: |
pip install .[dev]
pytest
pytest --doctest-modules
shell: bash

- name: Upload coverage to Codecov
Expand Down
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Unreleased

## 2.6.0
## 2.7.0

### Added

Expand All @@ -11,6 +11,14 @@

### Changed

- Redesigned parsing functions for short-hand declarations of array dimensions,
character length and parsing of kind
([#130](https://github.com/gnikit/fortls/pull/130))

## 2.6.0

### Changed

- Redesigned the `fortls` website to be more aesthetically pleasing and user-friendly
([#112](https://github.com/gnikit/fortls/issues/112))

Expand Down
1 change: 1 addition & 0 deletions fortls/ftypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class VarInfo:
#: keywords associated with this variable e.g. SAVE, DIMENSION, etc.
keywords: list[str] #: Keywords associated with variable
var_names: list[str] #: Variable names
var_kind: str = field(default=None) #: Kind of variable e.g. ``INTEGER*4`` etc.


@dataclass
Expand Down
18 changes: 18 additions & 0 deletions fortls/helper_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,24 @@ def get_keywords(keywords: list, keyword_info: dict = {}):
return keyword_strings


def parenthetic_contents(string: str):
"""Generate parenthesized contents in string as pairs
(contents, start-position, level).
Examples
--------
>>> list(parenthetic_contents('character*(10*size(val(1), 2)) :: name'))
[('1', 22, 2), ('val(1), 2', 18, 1), ('10*size(val(1), 2)', 10, 0)]
"""
stack = []
for i, c in enumerate(string):
if c == "(":
stack.append(i)
elif c == ")" and stack:
start = stack.pop()
yield (string[start + 1 : i], start, len(stack))


def get_paren_substring(string: str) -> str | None:
"""Get the contents enclosed by the first pair of parenthesis
Expand Down
155 changes: 110 additions & 45 deletions fortls/parse_fortran.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,47 +169,58 @@ def parse_var_keywords(test_str: str) -> tuple[list[str], str]:

def read_var_def(line: str, var_type: str = None, fun_only: bool = False):
"""Attempt to read variable definition line"""

def parse_kind(line: str):
match = FRegex.KIND_SPEC.match(line)
if not match:
return None, line
kind_str = match.group(1).replace(" ", "")
line = line[match.end(0) :]
if kind_str.find("(") >= 0:
match_char = find_paren_match(line)
if match_char < 0: # this triggers while typing with autocomplete
raise ValueError("Incomplete kind specification")
kind_str += line[: match_char + 1].strip()
line = line[match_char + 1 :]
return kind_str, line

if var_type is None:
type_match = FRegex.VAR.match(line)
if type_match is None:
return None
else:
var_type = type_match.group(0).strip()
trailing_line = line[type_match.end(0) :]
var_type = type_match.group(0).strip()
trailing_line = line[type_match.end(0) :]
else:
trailing_line = line[len(var_type) :]
var_type = var_type.upper()
trailing_line = trailing_line.split("!")[0]
if len(trailing_line) == 0:
return None
#
kind_match = FRegex.KIND_SPEC.match(trailing_line)
if kind_match:
kind_str = kind_match.group(1).replace(" ", "")
var_type += kind_str
trailing_line = trailing_line[kind_match.end(0) :]
if kind_str.find("(") >= 0:
match_char = find_paren_match(trailing_line)
if match_char < 0:
return None # Incomplete type spec
else:
kind_word = trailing_line[: match_char + 1].strip()
var_type += kind_word
trailing_line = trailing_line[match_char + 1 :]
else:
# Class and Type statements need a kind spec
if var_type in ("TYPE", "CLASS"):
return None
# Make sure next character is space or comma or colon
if not trailing_line[0] in (" ", ",", ":"):
return None

# Parse the global kind, if any, for the current line definition
# The global kind in some cases, like characters can be overriden by a locally
# defined kind
try:
kind_str, trailing_line = parse_kind(trailing_line)
var_type += kind_str # XXX: see below
except ValueError:
return None
except TypeError: # XXX: remove with explicit kind specification in VarInfo
pass

# Class and Type statements need a kind spec
if not kind_str and var_type in ("TYPE", "CLASS"):
return None
# Make sure next character is space or comma or colon
if not kind_str and not trailing_line[0] in (" ", ",", ":"):
return None
#
keywords, trailing_line = parse_var_keywords(trailing_line)
# Check if this is a function definition
fun_def = read_fun_def(trailing_line, ResultSig(type=var_type, keywords=keywords))
if (fun_def is not None) or fun_only:
if fun_def or fun_only:
return fun_def
#
# Split the type and variable name
line_split = trailing_line.split("::")
if len(line_split) == 1:
if len(keywords) > 0:
Expand All @@ -222,8 +233,8 @@ def read_var_def(line: str, var_type: str = None, fun_only: bool = False):
var_words = separate_def_list(trailing_line.strip())
if var_words is None:
var_words = []
#
return "var", VarInfo(var_type, keywords, var_words)

return "var", VarInfo(var_type, keywords, var_words, kind_str)


def get_procedure_modifiers(
Expand Down Expand Up @@ -1356,9 +1367,13 @@ def parse(
procedure_def = True
link_name = get_paren_substring(desc_string)
for var_name in obj_info.var_names:
desc = desc_string
link_name: str = None
if var_name.find("=>") > -1:
name_split = var_name.split("=>")
# TODO: rename name_raw to name
# TODO: rename name_stripped to name
# TODO: rename desc_string to desc
name_raw = name_split[0]
link_name = name_split[1].split("(")[0].strip()
if link_name.lower() == "null":
Expand All @@ -1367,28 +1382,27 @@ def parse(
name_raw = var_name.split("=")[0]
# Add dimension if specified
# TODO: turn into function and add support for co-arrays i.e. [*]
key_tmp = obj_info.keywords[:]
iparen = name_raw.find("(")
if iparen == 0:
# Copy global keywords to the individual variable
var_keywords: list[str] = obj_info.keywords[:]
# The name starts with (
if name_raw.find("(") == 0:
continue
elif iparen > 0:
if name_raw[iparen - 1] == "*":
iparen -= 1
if desc_string.find("(") < 0:
desc_string += f"*({get_paren_substring(name_raw)})"
else:
key_tmp.append(
f"dimension({get_paren_substring(name_raw)})"
)
name_raw = name_raw[:iparen]
name_raw, dims = self.parse_imp_dim(name_raw)
name_raw, char_len = self.parse_imp_char(name_raw)
if dims:
var_keywords.append(dims)
if char_len:
desc += char_len

name_stripped = name_raw.strip()
keywords, keyword_info = map_keywords(key_tmp)
keywords, keyword_info = map_keywords(var_keywords)

if procedure_def:
new_var = Method(
file_ast,
line_no,
name_stripped,
desc_string,
desc,
keywords,
keyword_info=keyword_info,
link_obj=link_name,
Expand All @@ -1398,9 +1412,10 @@ def parse(
file_ast,
line_no,
name_stripped,
desc_string,
desc,
keywords,
keyword_info=keyword_info,
# kind=obj_info.var_kind,
link_obj=link_name,
)
# If the object is fortran_var and a parameter include
Expand All @@ -1413,7 +1428,7 @@ def parse(
new_var.set_parameter_val(var)

# Check if the "variable" is external and if so cycle
if find_external(file_ast, desc_string, name_stripped, new_var):
if find_external(file_ast, desc, name_stripped, new_var):
continue

# if not merge_external:
Expand Down Expand Up @@ -1643,6 +1658,56 @@ def parse(
log.debug(f"{error['range']}: {error['message']}")
return file_ast

def parse_imp_dim(self, line: str):
"""Parse the implicit dimension of an array e.g.
var(3,4), var_name(size(val,1)*10)
Parameters
----------
line : str
line containing variable name
Returns
-------
tuple[str, str]
truncated line, dimension string
"""
m = re.compile(r"[ ]*\w+[ ]*(\()", re.I).match(line)
if not m:
return line, None
i = find_paren_match(line[m.end(1) :])
if i < 0:
return line, None # triggers for autocomplete
dims = line[m.start(1) : m.end(1) + i + 1]
line = line[: m.start(1)] + line[m.end(1) + i + 1 :]
return line, f"dimension{dims}"

def parse_imp_char(self, line: str):
"""Parse the implicit character length from a variable e.g.
var_name*10 or var_name*(10), var_name*(size(val, 1))
Parameters
----------
line : str
line containing potential variable
Returns
-------
tuple[str, str]
truncated line, character length
"""
match = re.compile(r"(\w+)[ ]*\*[ ]*(\d+|\()", re.I).match(line)
if not match:
return line, None
if match.group(2) == "(":
i = find_paren_match(line[match.end(2) :])
if i < 0:
return line, None # triggers for autocomplete
char_len = line[match.start(2) : match.end(2) + i + 1]
elif match.group(2).isdigit():
char_len = match.group(2)
return match.group(1), f"*{char_len}"

def parse_end_scope_word(
self, line: str, ln: int, file_ast: FortranAST, match: re.Match
) -> bool:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ profile = "black"

[tool.pytest.ini_options]
minversion = "7.0"
addopts = "-v --cov=fortls --cov-report=html --cov-report=xml --cov-context=test --doctest-modules"
addopts = "-v --cov=fortls --cov-report=html --cov-report=xml --cov-context=test"
testpaths = ["fortls", "test"]
Loading

0 comments on commit 573dc60

Please sign in to comment.