Skip to content

Commit

Permalink
feat: add --format flag and *ErrorsFormatter classes (#482)
Browse files Browse the repository at this point in the history
* move Errors class from file.py to errors.py

* feat: add --format flag and *ErrorsFormatter classes
  • Loading branch information
NiumXp authored Feb 1, 2024
1 parent 7e1a8b1 commit beaffaf
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 38 deletions.
11 changes: 11 additions & 0 deletions norminette/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)


Expand Down
110 changes: 110 additions & 0 deletions norminette/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from __future__ import annotations

import os
import json
from dataclasses import dataclass, field, asdict
from functools import cmp_to_key
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


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


@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:
__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)

# TODO Add `add(...)` method to allow creating `Highlight`s and `Error`s easily

@property
def status(self) -> Literal["OK", "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,
)
37 changes: 2 additions & 35 deletions norminette/file.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
3 changes: 0 additions & 3 deletions norminette/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
3 changes: 3 additions & 0 deletions tests/rules/rules_generator_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
47 changes: 47 additions & 0 deletions tests/test_errors.py
Original file line number Diff line number Diff line change
@@ -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=",:")

0 comments on commit beaffaf

Please sign in to comment.