Skip to content

Feature/kind-improvements #130

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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