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

feat: add --format flag and *ErrorsFormatter classes #482

Merged
merged 2 commits into from
Feb 1, 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
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=",:")
Loading