Skip to content

Commit

Permalink
Enable deactivation of style checks
Browse files Browse the repository at this point in the history
Ref. #1079
  • Loading branch information
treiher committed Jun 21, 2022
1 parent 2899278 commit 1d04f39
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 33 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Memory management in sessions to avoid use of heap (#629)
- Setting of single message fields (#1067)

Specification:

- Enable deactivation of style checks for individual files (#1079)

CLI:

- `rflx` option `--max-errors NUM` (#748)
Expand Down
20 changes: 19 additions & 1 deletion doc/User-Guide.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,25 @@
:toc:
:numbered:

== Integration files
== Specification Files

=== Style Checks

By default, the style of specification files is checked. Style checks can be disabled for individual files by adding a pragma to the first line of the file. Besides the deactivation of specific checks, it is also possible to disable all checks by using `all`.

*Example*

[source,ada,rflx]
----
-- style: disable = line-length, blank-lines
package P is
...
----


== Integration Files

For each RecordFlux specification file with the `.rflx` file extension, users
may provide a file with the same name but the `.rfi` file extension. This is
Expand Down
117 changes: 97 additions & 20 deletions rflx/specification/style.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import re
from enum import Enum
from pathlib import Path

from rflx.error import Location, RecordFluxError, Severity, Subsystem
Expand All @@ -26,6 +27,16 @@
]


class Check(Enum):
ALL = "all"
BLANK_LINES = "blank-lines"
CHARACTERS = "characters"
INDENTATION = "indentation"
LINE_LENGTH = "line-length"
TOKEN_SPACING = "token-spacing"
TRAILING_SPACES = "trailing-spaces"


def check(spec_file: Path) -> RecordFluxError:
error = RecordFluxError()

Expand All @@ -35,25 +46,43 @@ def check(spec_file: Path) -> RecordFluxError:
if not specification:
return error

blank_lines = 0
lines = specification.split("\n")
enabled_checks = _determine_enabled_checks(error, lines[0], spec_file)

if not enabled_checks:
return error

blank_lines = 0

for i, l in enumerate(lines, start=1):
blank_lines = _check_blank_lines(error, l, i, spec_file, blank_lines, len(lines))
_check_characters(error, l, i, spec_file)
_check_indentation(error, l, i, spec_file)
_check_line_length(error, l, i, spec_file)
_check_token_spacing(error, l, i, spec_file)
_check_trailing_spaces(error, l, i, spec_file)
if Check.BLANK_LINES in enabled_checks:
blank_lines = _check_blank_lines(error, l, i, spec_file, blank_lines, len(lines))
if Check.CHARACTERS in enabled_checks:
_check_characters(error, l, i, spec_file)
if Check.INDENTATION in enabled_checks:
_check_indentation(error, l, i, spec_file)
if Check.LINE_LENGTH in enabled_checks:
_check_line_length(error, l, i, spec_file)
if Check.TOKEN_SPACING in enabled_checks:
_check_token_spacing(error, l, i, spec_file)
if Check.TRAILING_SPACES in enabled_checks:
_check_trailing_spaces(error, l, i, spec_file)

return error


def _append(error: RecordFluxError, message: str, row: int, col: int, spec_file: Path) -> None:
def _append(
error: RecordFluxError,
message: str,
row: int,
col: int,
spec_file: Path,
check_type: Check = None,
) -> None:
error.extend(
[
(
message,
message + (f" [{check_type.value}]" if check_type else ""),
Subsystem.STYLE,
Severity.ERROR,
Location((row, col), spec_file),
Expand All @@ -62,18 +91,37 @@ def _append(error: RecordFluxError, message: str, row: int, col: int, spec_file:
)


def _determine_enabled_checks(error: RecordFluxError, line: str, spec_file: Path) -> set[Check]:
checks = {c.value for c in Check.__members__.values()}
disabled_checks = set()

if "-- style:" in line:
m = re.match(r"^-- style: disable = ([^.]*)$", line)
if m:
disabled_checks = {c.strip() for c in m.group(1).split(",")}
for c in disabled_checks - checks:
_append(error, f'invalid check "{c}"', 1, 1, spec_file)
else:
_append(error, "invalid format of style pragma", 1, 1, spec_file)

if Check.ALL.value in disabled_checks:
return set()

return {Check(c) for c in checks - disabled_checks}


def _check_blank_lines(
error: RecordFluxError, line: str, row: int, spec_file: Path, blank_lines: int, row_count: int
) -> int:
if line == "":
if row == 1:
_append(error, "leading blank line", row, 1, spec_file)
_append(error, "leading blank line", row, 1, spec_file, Check.BLANK_LINES)
if blank_lines > 0 and row == row_count:
_append(error, "trailing blank line", row - 1, 1, spec_file)
_append(error, "trailing blank line", row - 1, 1, spec_file, Check.BLANK_LINES)
blank_lines += 1
else:
if blank_lines > 1:
_append(error, "multiple blank lines", row - 1, 1, spec_file)
_append(error, "multiple blank lines", row - 1, 1, spec_file, Check.BLANK_LINES)
blank_lines = 0

return blank_lines
Expand All @@ -83,10 +131,10 @@ def _check_characters(error: RecordFluxError, line: str, row: int, spec_file: Pa
for j, c in enumerate(line, start=1):
if c == INCORRECT_LINE_TERMINATORS:
s = repr(c).replace("'", '"')
_append(error, f"incorrect line terminator {s}", row, j, spec_file)
_append(error, f"incorrect line terminator {s}", row, j, spec_file, Check.CHARACTERS)
if c in ILLEGAL_WHITESPACE_CHARACTERS:
s = repr(c).replace("'", '"')
_append(error, f"illegal whitespace character {s}", row, j, spec_file)
_append(error, f"illegal whitespace character {s}", row, j, spec_file, Check.CHARACTERS)


def _check_indentation(error: RecordFluxError, line: str, row: int, spec_file: Path) -> None:
Expand All @@ -107,6 +155,7 @@ def _check_indentation(error: RecordFluxError, line: str, row: int, spec_file: P
row,
match.end(),
spec_file,
Check.INDENTATION,
)


Expand Down Expand Up @@ -143,23 +192,51 @@ def _check_token_spacing(error: RecordFluxError, line: str, row: int, spec_file:
if space_before:
assert token != ";"
if match.start() > 1 and line[match.start() - 1] not in " (":
_append(error, f'missing space before "{token}"', row, match.start() + 1, spec_file)
_append(
error,
f'missing space before "{token}"',
row,
match.start() + 1,
spec_file,
Check.TOKEN_SPACING,
)
else:
if match.start() > 1 and line[match.start() - 1] == " ":
_append(error, f'space before "{token}"', row, match.start() + 1, spec_file)
_append(
error,
f'space before "{token}"',
row,
match.start() + 1,
spec_file,
Check.TOKEN_SPACING,
)
if space_after:
if match.end() < len(line) and line[match.end()] not in " ;\n":
_append(error, f'missing space after "{token}"', row, match.end() + 1, spec_file)
_append(
error,
f'missing space after "{token}"',
row,
match.end() + 1,
spec_file,
Check.TOKEN_SPACING,
)
else:
if match.end() < len(line) and line[match.end()] == " ":
_append(error, f'space after "{token}"', row, match.end() + 2, spec_file)
_append(
error,
f'space after "{token}"',
row,
match.end() + 2,
spec_file,
Check.TOKEN_SPACING,
)


def _check_trailing_spaces(error: RecordFluxError, line: str, row: int, spec_file: Path) -> None:
if line.endswith(" "):
_append(error, "trailing whitespace", row, len(line), spec_file)
_append(error, "trailing whitespace", row, len(line), spec_file, Check.TRAILING_SPACES)


def _check_line_length(error: RecordFluxError, line: str, row: int, spec_file: Path) -> None:
if len(line) > 120:
_append(error, f"line too long ({len(line)}/120)", row, 121, spec_file)
_append(error, f"line too long ({len(line)}/120)", row, 121, spec_file, Check.LINE_LENGTH)
110 changes: 98 additions & 12 deletions tests/unit/specification/style_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,57 +90,66 @@ def test_no_error(spec: str, tmp_path: Path) -> None:
[
(
"\npackage Test is end Test;",
r"1:1: style: error: leading blank line",
r"1:1: style: error: leading blank line \[blank-lines\]",
),
(
"package Test is end Test;\n\n",
r"2:1: style: error: trailing blank line",
r"2:1: style: error: trailing blank line \[blank-lines\]",
),
(
"package Test is\n\n\nend Test;",
r"3:1: style: error: multiple blank lines",
r"3:1: style: error: multiple blank lines \[blank-lines\]",
),
(
"""package Test is\tend Test;""",
r'1:16: style: error: illegal whitespace character "\\t"',
r'1:16: style: error: illegal whitespace character "\\t" \[characters\]',
),
(
"package Test is\r\nend Test;",
r'1:16: style: error: incorrect line terminator "\\r"',
r'1:16: style: error: incorrect line terminator "\\r" \[characters\]',
),
(
"package Test is end Test; ",
r"1:26: style: error: trailing whitespace",
r"1:26: style: error: trailing whitespace \[trailing-spaces\]",
),
(
"package Test is\n type T is mod 2 ** 16;\nend Test;",
r"2:9: style: error: unexpected keyword indentation \(expected 3 or 6\)",
r"2:9: style: error: unexpected keyword indentation \(expected 3 or 6\)"
r" \[indentation\]",
),
(
"package Test is\n type T is mod 2* 128;\nend Test;",
r'2:19: style: error: missing space before "\*"',
r'2:19: style: error: missing space before "\*" \[token-spacing\]',
),
(
"package Test is end Test; --A test package",
r'1:29: style: error: missing space after "--"',
r'1:29: style: error: missing space after "--" \[token-spacing\]',
),
(
f"package Test is end Test; -- {'X' * 100}",
r"1:121: style: error: line too long \(129/120\)",
r"1:121: style: error: line too long \(129/120\) \[line-length\]",
),
(
"package Test is\n"
" type E is mod 2 ** 16;\n"
" type S is sequence of Test ::E;\n"
"end Test;",
r'3:31: style: error: space before "::"',
r'3:31: style: error: space before "::" \[token-spacing\]',
),
(
"package Test is\n"
" type E is mod 2 ** 16;\n"
" type S is sequence of Test:: E;\n"
"end Test;",
r'3:33: style: error: space after "::"',
r'3:33: style: error: space after "::" \[token-spacing\]',
),
(
"-- style: disable",
r"1:1: style: error: invalid format of style pragma",
),
(
"-- style: disable = foo",
r'1:1: style: error: invalid check "foo"',
),
],
)
Expand All @@ -149,3 +158,80 @@ def test_error(tmp_path: Path, spec: str, error: str) -> None:
spec_file.write_text(spec)
with pytest.raises(RecordFluxError, match=rf"^{spec_file}:{error}$"):
style.check(spec_file).propagate()


@pytest.mark.parametrize(
"spec, disabled_checks",
[
(
"package Test is end Test;\n\n",
"blank-lines",
),
(
"package Test is\n\n\nend Test;",
"blank-lines",
),
(
"""package Test is\tend Test;""",
"characters",
),
(
"package Test is\r\nend Test;",
"characters",
),
(
"package Test is end Test; ",
"trailing-spaces",
),
(
"package Test is\n type T is mod 2 ** 16;\nend Test;",
"indentation",
),
(
"package Test is\n type T is mod 2* 128;\nend Test;",
"token-spacing",
),
(
"package Test is end Test; --A test package",
"token-spacing",
),
(
f"package Test is end Test; -- {'X' * 100}",
"line-length",
),
(
"package Test is\n"
" type E is mod 2 ** 16;\n"
" type S is sequence of Test ::E;\n"
"end Test;",
"token-spacing",
),
(
"package Test is\n"
" type E is mod 2 ** 16;\n"
" type S is sequence of Test:: E;\n"
"end Test;",
"token-spacing",
),
(
"package Test is \n"
" type E is mod 2 ** 16;\r\n"
" type S is sequence of Test:: E;\n"
"end Test;\n\n",
"all",
),
(
"package Test is \n"
" type E is mod 2 ** 16;\r\n"
" type S is sequence of Test:: E;\n"
"end Test;\n\n",
"blank-lines, characters, indentation, token-spacing, trailing-spaces",
),
],
)
def test_deactivation_of_checks_on_file_level(
spec: str, disabled_checks: str, tmp_path: Path
) -> None:
spec_file = tmp_path / "test.rflx"
spec_file.write_text(f"-- style: disable = {disabled_checks}\n\n{spec}")
style.check(spec_file).propagate()

0 comments on commit 1d04f39

Please sign in to comment.