Skip to content
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

Feature/parentheses #4

Merged
merged 3 commits into from
Mar 11, 2024
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ The following universal `.editorconfig` fields are considered:
* `insert_final_newline`
* If true, every code block must end with a newline

And The following unofficial (custom) `.editorconfig` fields are used:

* `parentheses_conditionals`
* If true, parentheses are enforced inside if-statements (`IF (condition = 1) THEN...`)
* If false, parentheses inside if-statements are removed (`IF condition = 1 THEN...`)

When a config property is not set, the formatter will typically take no action.
For example, not specifying `indent_style` (or using `unset`) will result in no whitespace conversions at all.

Expand Down
2 changes: 2 additions & 0 deletions src/tctools/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
FormatInsertFinalNewline,
FormatEndOfLine,
FormatVariablesAlign,
FormatConditionalParentheses,
)


Expand Down Expand Up @@ -46,6 +47,7 @@ def main(*args) -> int:
Formatter.register_rule(FormatInsertFinalNewline)
Formatter.register_rule(FormatEndOfLine)
Formatter.register_rule(FormatVariablesAlign)
Formatter.register_rule(FormatConditionalParentheses)

formatter = Formatter(
quiet=arguments.quiet,
Expand Down
72 changes: 72 additions & 0 deletions src/tctools/format_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,3 +326,75 @@ def _pad_to_indent_level(self, line: str, tab_index: int):
padding_str += new_indent

return padding_str


class FormatConditionalParentheses(FormattingRule):
"""Formatter to make uses of parentheses inside IF, CASE and WHILE consistent."""

def __init__(self, *args):
super().__init__(*args)

self._parentheses = self._properties.get("parentheses_conditionals", None)

# Regex to find conditional inside single lines:

self._re_needs_parentheses = re.compile(
r"""
# Look for start of string or new line:
^
# Match keyword with surrounding ws:
\s*(?:IF|WHILE|CASE)\s+
# Match any characters NOT starting with "("
# We cannot match the closing bracket, as this could be from a
# function call
([^(\r\n].+?)
# Match keyword with preceding ws:
\s+(?:THEN|DO|OF)
""",
re.VERBOSE | re.MULTILINE,
)

self._re_removes_parentheses = re.compile(
r"""
# Look for start of string or new line:
^
# Match IF with surrounding ws:
\s*(?:IF|WHILE|CASE)\s*
# Match any characters within ():
\((.+)\)
# Match THEN with preceding ws:
\s*(?:THEN|DO|OF)
""",
re.VERBOSE | re.MULTILINE,
)

def format(self, content: List[str], kind: Optional[Kind] = None):
if self._parentheses is None:
return # Nothing to do

if self._parentheses:
pattern = self._re_needs_parentheses
else:
pattern = self._re_removes_parentheses

for i, line in enumerate(content):
# Do a manual match + replace, instead of e.g. subn(), because we might
# need to add extra spaces after removing parentheses
if match := pattern.search(line):
prefix = line[: match.start(1)]
condition = match.group(1)
suffix = line[match.end(1) :]

if self._parentheses:
condition = "(" + condition + ")"
else:
prefix = prefix[:-1] # Remove parentheses
suffix = suffix[1:]

# Removing the () could cause a syntax error:
if not prefix.endswith(" "):
prefix += " "
if not suffix.startswith(" "):
suffix = " " + suffix

content[i] = prefix + condition + suffix
100 changes: 92 additions & 8 deletions tests/test_formatting_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,9 @@ def test_final_newline(content, expected):
properties = {"insert_final_newline": True}

rule = format_rules.FormatInsertFinalNewline(properties)
rule.format(content)

assert content == expected
content_new = content.copy()
rule.format(content_new)
assert content_new == expected


content_eol = [
Expand Down Expand Up @@ -170,9 +170,9 @@ def test_end_of_line(eol, expected):

content = content_before.copy()
rule = format_rules.FormatEndOfLine({"end_of_line": eol})
rule.format(content)

assert content == expected
content_new = content.copy()
rule.format(content_new)
assert content_new == expected


content_variables = [
Expand Down Expand Up @@ -245,5 +245,89 @@ def test_end_of_line(eol, expected):
@pytest.mark.parametrize("content,expected,settings", content_variables)
def test_variable_align(content, expected, settings):
rule = format_rules.FormatVariablesAlign(settings)
rule.format(content, Kind.DECLARATION)
assert content == expected
content_new = content.copy()
rule.format(content_new, Kind.DECLARATION)
assert content_new == expected


content_parentheses = [
(
[
"IF inputs.button = 1 THEN\n",
" output.led := 1;\n",
"END_IF\n",
],
[
"IF (inputs.button = 1) THEN\n",
" output.led := 1;\n",
"END_IF\n",
],
),
(
[
"IF inputs.button = 1 THEN // comment!\n",
],
[
"IF (inputs.button = 1) THEN // comment!\n",
],
),
(
[
"IF func(arg1 := 1, args2 := func2()) THEN\n",
],
[
"IF (func(arg1 := 1, args2 := func2())) THEN\n",
],
),
(
[
"WHILE func() DO // comment!\n",
],
[
"WHILE (func()) DO // comment!\n",
],
),
(
[
"CASE idx OF\n",
],
[
"CASE (idx) OF\n",
],
),
# ( # This case fails, because we cannot identify matching parentheses:
# [
# "IF (1+1)*2 = 3*(x-1) THEN\n",
# ],
# [
# "IF ((1+1)*2 = 3*(x-1)) THEN\n",
# ],
# ),
]


@pytest.mark.parametrize("content,expected", content_parentheses)
def test_parentheses_add(content, expected):
rule = format_rules.FormatConditionalParentheses({"parentheses_conditionals": True})
content_new = content.copy()
rule.format(content_new)
assert content_new == expected


@pytest.mark.parametrize("expected,content", content_parentheses)
def test_parentheses_remove(expected, content):
rule = format_rules.FormatConditionalParentheses(
{"parentheses_conditionals": False}
)
content_new = content.copy()
rule.format(content_new)
assert content_new == expected


def test_parentheses_remove_no_ws():
rule = format_rules.FormatConditionalParentheses(
{"parentheses_conditionals": False}
)
content = ["IF(inputs.button = 1)THEN"]
rule.format(content)
assert content == ["IF inputs.button = 1 THEN"]
Loading