From e99839b8dbfe85bf8f194b77a13a8b42d79269ba Mon Sep 17 00:00:00 2001 From: NiumXp Date: Tue, 23 Jan 2024 19:38:36 -0300 Subject: [PATCH 1/2] move Errors class from file.py to errors.py --- norminette/errors.py | 36 ++++++++++++++++++++++++++++++++++++ norminette/file.py | 37 ++----------------------------------- 2 files changed, 38 insertions(+), 35 deletions(-) create mode 100644 norminette/errors.py diff --git a/norminette/errors.py b/norminette/errors.py new file mode 100644 index 0000000..0038a37 --- /dev/null +++ b/norminette/errors.py @@ -0,0 +1,36 @@ +from functools import cmp_to_key +from typing import Union, Literal + +from norminette.norm_error import NormError, NormWarning + + +def sort_errs(a, b): + if a.col == b.col and a.line == b.line: + return 1 if a.errno > b.errno else -1 + return a.col - b.col if a.line == b.line else a.line - b.line + + +class Errors: + __slots__ = "_inner" + + def __init__(self) -> None: + self._inner = [] + + def __len__(self) -> int: + return len(self._inner) + + def __iter__(self): + self._inner.sort(key=cmp_to_key(sort_errs)) + return iter(self._inner) + + @property + def status(self) -> Literal["OK", "Error"]: + if not self: + return "OK" + if all(isinstance(it, NormWarning) for it in self): + return "OK" + return "Error" + + def append(self, value: Union[NormError, NormWarning]) -> None: + assert isinstance(value, (NormError, NormWarning)) + self._inner.append(value) diff --git a/norminette/file.py b/norminette/file.py index 5d12be2..ac44a11 100644 --- a/norminette/file.py +++ b/norminette/file.py @@ -1,40 +1,7 @@ import os -from functools import cmp_to_key -from typing import Optional, Union, Literal +from typing import Optional -from norminette.norm_error import NormError, NormWarning - - -def sort_errs(a, b): - if a.col == b.col and a.line == b.line: - return 1 if a.errno > b.errno else -1 - return a.col - b.col if a.line == b.line else a.line - b.line - - -class Errors: - __slots__ = "_inner" - - def __init__(self) -> None: - self._inner = [] - - def __len__(self) -> int: - return len(self._inner) - - def __iter__(self): - self._inner.sort(key=cmp_to_key(sort_errs)) - return iter(self._inner) - - @property - def status(self) -> Literal["OK", "Error"]: - if not self: - return "OK" - if all(isinstance(it, NormWarning) for it in self): - return "OK" - return "Error" - - def append(self, value: Union[NormError, NormWarning]) -> None: - assert isinstance(value, (NormError, NormWarning)) - self._inner.append(value) +from norminette.errors import Errors class File: From 74bd6ed1dc6ed9942da1b6b00a10a89fd474fbf4 Mon Sep 17 00:00:00 2001 From: NiumXp Date: Wed, 24 Jan 2024 00:51:53 -0300 Subject: [PATCH 2/2] feat: add --format flag and *ErrorsFormatter classes --- norminette/__main__.py | 11 ++++ norminette/errors.py | 96 +++++++++++++++++++++++++---- norminette/registry.py | 3 - tests/rules/rules_generator_test.py | 3 + tests/test_errors.py | 47 ++++++++++++++ 5 files changed, 146 insertions(+), 14 deletions(-) create mode 100644 tests/test_errors.py diff --git a/norminette/__main__.py b/norminette/__main__.py index 8e0fede..632025d 100644 --- a/norminette/__main__.py +++ b/norminette/__main__.py @@ -4,6 +4,7 @@ from importlib.metadata import version import argparse +from norminette.errors import formatters from norminette.file import File from norminette.lexer import Lexer, TokenError from norminette.exceptions import CParsingError @@ -61,10 +62,18 @@ def main(): action="store_true", help="Parse only source files not match to .gitignore", ) + parser.add_argument( + "-f", + "--format", + choices=list(formatter.name for formatter in formatters), + help="formatting style for errors", + default="humanized", + ) parser.add_argument("-R", nargs=1, help="compatibility for norminette 2") args = parser.parse_args() registry = Registry() + format = next(filter(lambda it: it.name == args.format, formatters)) files = [] debug = args.debug if args.cfile or args.hfile: @@ -121,6 +130,8 @@ def main(): sys.exit(1) except KeyboardInterrupt: sys.exit(1) + errors = format(files) + print(errors) sys.exit(1 if len(file.errors) else 0) diff --git a/norminette/errors.py b/norminette/errors.py index 0038a37..dbe436d 100644 --- a/norminette/errors.py +++ b/norminette/errors.py @@ -1,13 +1,40 @@ +from __future__ import annotations + +import os +import json +from dataclasses import dataclass, field, asdict from functools import cmp_to_key -from typing import Union, Literal +from typing import TYPE_CHECKING, Sequence, Union, Literal, Optional, List + +from norminette.norm_error import NormError, NormWarning, errors as errors_dict + +if TYPE_CHECKING: + from norminette.file import File + -from norminette.norm_error import NormError, NormWarning +def sort_errs(a: Error, b: Error): + # TODO Add to Error and Highlight dataclasses be sortable to remove this fn + ah: Highlight = a.highlights[0] + bh: Highlight = b.highlights[0] + if ah.column == bh.column and ah.lineno == bh.lineno: + return 1 if a.name > b.name else -1 + return ah.column - bh.column if ah.lineno == bh.lineno else ah.lineno - bh.lineno -def sort_errs(a, b): - if a.col == b.col and a.line == b.line: - return 1 if a.errno > b.errno else -1 - return a.col - b.col if a.line == b.line else a.line - b.line +@dataclass +class Highlight: + lineno: int + column: int + length: Optional[int] = field(default=None) + hint: Optional[str] = field(default=None) + + +@dataclass +class Error: + name: str + text: str + level: Literal["Error", "Notice"] + highlights: List[Highlight] class Errors: @@ -23,14 +50,61 @@ def __iter__(self): self._inner.sort(key=cmp_to_key(sort_errs)) return iter(self._inner) + # TODO Add `add(...)` method to allow creating `Highlight`s and `Error`s easily + @property def status(self) -> Literal["OK", "Error"]: - if not self: - return "OK" - if all(isinstance(it, NormWarning) for it in self): - return "OK" - return "Error" + return "OK" if all(it.level == "Notice" for it in self._inner) else "Error" def append(self, value: Union[NormError, NormWarning]) -> None: + # TODO Remove NormError and NormWarning since it does not provide `length` data assert isinstance(value, (NormError, NormWarning)) + level = "Error" if isinstance(value, NormError) else "Notice" + value = Error(value.errno, value.error_msg, level, highlights=[ + Highlight(value.line, value.col, None), + ]) self._inner.append(value) + + +class _formatter: + def __init__(self, files: Union[File, Sequence[File]]) -> None: + if not isinstance(files, list): + files = [files] + self.files = files + + def __init_subclass__(cls) -> None: + cls.name = cls.__name__.rstrip("ErrorsFormatter").lower() + + +class HumanizedErrorsFormatter(_formatter): + def __str__(self) -> str: + output = '' + for file in self.files: + output += f"{file.basename}: {file.errors.status}!" + for error in file.errors: + brief = errors_dict.get(error.name, "Error not found") + highlight = error.highlights[0] + output += f"\n{error.level}: {error.name:<20} " + output += f"(line: {highlight.lineno:>3}, col: {highlight.column:>3}):\t{brief}" + return output + + +class JSONErrorsFormatter(_formatter): + def __str__(self): + files = [] + for file in self.files: + files.append({ + "path": os.path.abspath(file.path), + "status": file.errors.status, + "errors": tuple(map(asdict, file.errors)), + }) + output = { + "files": files, + } + return json.dumps(output, separators=",:") + + +formatters = ( + JSONErrorsFormatter, + HumanizedErrorsFormatter, +) diff --git a/norminette/registry.py b/norminette/registry.py index 9d1ac25..3d9b5e1 100644 --- a/norminette/registry.py +++ b/norminette/registry.py @@ -78,6 +78,3 @@ def run(self, context): print(context.debug) if context.debug > 0: print("uncaught ->", unrecognized_tkns) - print(f"{context.file.basename}: {context.file.errors.status}!") - for error in context.file.errors: - print(error) diff --git a/tests/rules/rules_generator_test.py b/tests/rules/rules_generator_test.py index 703720f..f2049ad 100644 --- a/tests/rules/rules_generator_test.py +++ b/tests/rules/rules_generator_test.py @@ -5,6 +5,7 @@ from norminette.lexer import Lexer from norminette.context import Context from norminette.registry import Registry +from norminette.errors import HumanizedErrorsFormatter registry = Registry() @@ -23,6 +24,8 @@ def test_rule_for_file(file, capsys): lexer = Lexer(file) context = Context(file, lexer.get_tokens(), debug=2) registry.run(context) + errors = HumanizedErrorsFormatter(file) + print(errors) captured = capsys.readouterr() assert captured.out == out_content diff --git a/tests/test_errors.py b/tests/test_errors.py new file mode 100644 index 0000000..9f61515 --- /dev/null +++ b/tests/test_errors.py @@ -0,0 +1,47 @@ +import json + +import pytest + +from norminette.file import File +from norminette.lexer import Lexer +from norminette.context import Context +from norminette.registry import Registry +from norminette.errors import JSONErrorsFormatter + +tests = [ + { + "file": File("/nium/test.c", "int\tmain()\n{\n\treturn ;\n}\n"), + "test": { + "files": [ + { + "path": "/nium/test.c", + "status": "Error", + "errors": [ + { + "name": "INVALID_HEADER", + "text": "Missing or invalid 42 header", + "level": "Error", + "highlights": [{"lineno": 1, "column": 1, "length": None, "hint": None}], + }, + { + "name": "NO_ARGS_VOID", + "text": "Empty function argument requires void", + "level": "Error", + "highlights": [{"lineno": 1, "column": 10, "length": None, "hint": None}], + }, + ], + }, + ], + }, + }, +] + + +@pytest.mark.parametrize("file,test", [it.values() for it in tests]) +def test_json_formatter_errored_file(file, test): + lexer = Lexer(file) + context = Context(file, lexer.get_tokens()) + Registry().run(context) + + formatter = JSONErrorsFormatter(file) + assert str(formatter) == json.dumps(test, separators=",:")