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

Add a new user/beginner friendly errors system #505

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
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
44 changes: 0 additions & 44 deletions norminette/colors.py

This file was deleted.

40 changes: 39 additions & 1 deletion norminette/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from norminette.norm_error import NormError, NormWarning, errors as errors_dict

if TYPE_CHECKING:
from norminette.lexer import Token
from norminette.file import File


Expand All @@ -36,6 +37,14 @@ def __lt__(self, other: Any) -> bool:
return self.column > other.column
return self.lineno > other.lineno

@classmethod
def from_token(cls, token: Token, /) -> Highlight:
return cls(
lineno=token.lineno,
column=token.column,
length=token.length,
)


@dataclass
class Error:
Expand Down Expand Up @@ -69,10 +78,14 @@ def add_highlight(
) -> None: ...
@overload
def add_highlight(self, highlight: Highlight, /) -> None: ...
@overload
def add_highlight(self, token: Token, /) -> None: ...

def add_highlight(self, *args, **kwargs) -> None:
if len(args) == 1:
highlight, = args
if not isinstance(highlight, Highlight): # highlight is Token
highlight = Highlight.from_token(highlight)
else:
highlight = Highlight(*args, **kwargs)
self.highlights.append(highlight)
Expand Down Expand Up @@ -167,10 +180,34 @@ def __init__(self, files: Union[File, Sequence[File]]) -> None:
self.files = files

def __init_subclass__(cls) -> None:
cls.name = cls.__name__.rstrip("ErrorsFormatter").lower()
name = cls.__name__
if name.endswith(suffix := "ErrorsFormatter"):
name = name[:-len(suffix)]
cls.name = name.lower()


class HumanizedErrorsFormatter(_formatter):
def __str__(self) -> str:
output = ''
for file in self.files:
for error in file.errors:
highlight = error.highlights[0]
# Location
output += f"\x1b[;97m{file.path}:{highlight.lineno}:{highlight.column}\x1b[0m"
output += ' ' + error.name
if not highlight.length:
output += ' ' + error.text
if highlight.length:
# Line
output += f"\n {highlight.lineno:>5} | {file[highlight.lineno,].translated}"
# Arrow
output += "\n | " + ' ' * (highlight.column - 1)
output += f"\x1b[0;91m{'^' * (highlight.length or 0)} {highlight.hint or error.text}\x1b[0m"
output += '\n'
return output


class ShortErrorsFormatter(_formatter):
def __str__(self) -> str:
output = ''
for file in self.files:
Expand Down Expand Up @@ -200,5 +237,6 @@ def __str__(self):

formatters = (
JSONErrorsFormatter,
ShortErrorsFormatter,
HumanizedErrorsFormatter,
)
81 changes: 79 additions & 2 deletions norminette/file.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,101 @@
import os
from typing import Optional
from typing import Optional, Any, Sequence, overload, Tuple
from functools import cached_property

from norminette.errors import Errors
from norminette.exceptions import MaybeInfiniteLoop
from norminette.utils import max_loop_iterations


class _Line:
def __init__(self, inner: str, /) -> None:
self._inner = inner

@property
def translated(self) -> str:
result = ''
for char in self._inner:
if char == '\n':
break
if char == '\t':
char = ' ' * (4 - len(result) % 4)
result += char
return result

def endswith(self, value: Sequence[str], /) -> bool:
if isinstance(value, str):
value = value,
for value in value:
if self._inner.endswith('\n'):
value += '\n'
if self._inner.endswith(value):
return True
return False


class File:
def __init__(self, path: str, source: Optional[str] = None) -> None:
self.path = path
self._source = source
self._line_to_index = {}

self.errors = Errors()
self.basename = os.path.basename(path)
self.name, self.type = os.path.splitext(self.basename)

@property
@cached_property
def source(self) -> str:
if self._source is None:
with open(self.path) as file:
self._source = file.read()
lineno = 1
offset = None
self._line_to_index = {1: 0}
for _ in range(max_loop_iterations):
offset = self._source.find('\n', offset)
if offset == -1:
break
offset += 1
lineno += 1
self._line_to_index[lineno] = offset
else:
raise MaybeInfiniteLoop()
return self._source

def _line(self, lineno: int, /) -> Optional[_Line]:
if lineno not in self._line_to_index:
return
offset = self._line_to_index[lineno]
ending = self.source.find('\n', offset)
if ending == -1:
ending = len(self.source)
else:
ending += 1
return _Line(self.source[offset:ending])

def __iter__(self):
lineno = 1
while (line := self._line(lineno)) is not None:
yield line
lineno += 1
return

def __repr__(self) -> str:
return f"<File {self.path!r}>"

@overload
def __getitem__(self, item: int) -> str: ...
@overload
def __getitem__(self, item: Tuple[int]) -> _Line: ...

def __getitem__(self, item: Any):
if isinstance(item, int):
return self.source[item]
if isinstance(item, tuple):
if len(item) == 1:
lineno = item[0]
return self._line(lineno)
# if len(item) == 2:
# lineno, column = item
# return self._line_to_index[lineno + column]
raise TypeError(...)
8 changes: 8 additions & 0 deletions norminette/lexer/tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ class Token:
def length(self) -> int:
return len(self.value or '')

@property
def lineno(self) -> int:
return self.pos[0]

@property
def column(self) -> int:
return self.pos[1]

@property
def line_column(self):
return self.pos[1]
Expand Down
21 changes: 1 addition & 20 deletions norminette/norm_error.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
from norminette.colors import red, blue, yellow, pink, grey, green

errors = {
errors = {
"SPC_INSTEAD_TAB": "Spaces at beginning of line",
"TAB_INSTEAD_SPC": "Found tab when expecting space",
"CONSECUTIVE_SPC": "Two or more consecutives spaces",
Expand Down Expand Up @@ -152,28 +150,11 @@ def __init__(self, errno, line, col):
self.error_pos = f"(line: {(str(self.line)).rjust(3)}, col: {(str(self.col)).rjust(3)}):\t"
self.prefix = f"Error: {self.errno:<20} {self.error_pos:>21}"
self.error_msg = f"{errors.get(self.errno, 'ERROR NOT FOUND')}"
self.no_color = "\033[0m"
self.color = self.no_color
color = {
"\033[91m":red,
"\033[92m":green,
"\033[93m":yellow,
"\033[94m":blue,
"\033[95m":pink,
"\033[97m":grey,
}
for key,value in color.items():
if errno in value:
self.color = key
break
self.prefix = self.prefix
self.error_msg = self.color + self.error_msg + self.no_color

def __str__(self):
return self.prefix + self.error_msg



class NormWarning:
def __init__(self, errno, line, col):
self.errno = errno
Expand Down
2 changes: 0 additions & 2 deletions norminette/rules/check_brace.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ def run(self, context):
return False, 0
i += 1
i = context.skip_ws(i, nl=False)
if context.check_token(i, "NEWLINE") is True and context.check_token(i - 1, ["SPACE", "TAB"]):
context.new_error("SPC_BEFORE_NL", context.peek_token(i - 1))
if context.check_token(i, "NEWLINE") is False or context.check_token(i, "NEWLINE") is None:
if context.scope.name == "UserDefinedType" or context.scope.name == "UserDefinedEnum":
i = context.skip_ws(i, nl=False)
Expand Down
29 changes: 0 additions & 29 deletions norminette/rules/check_comment_line_len.py

This file was deleted.

2 changes: 0 additions & 2 deletions norminette/rules/check_empty_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@ def run(self, context):
context.new_error("NL_AFTER_PREPROC", context.peek_token(i))
if context.history[-1] != "IsEmptyLine":
return False, 0
if context.check_token(i, "NEWLINE") is False:
context.new_error("SPACE_EMPTY_LINE", context.peek_token(i))
if context.history[-2] == "IsEmptyLine":
context.new_error("CONSECUTIVE_NEWLINES", context.peek_token(i))
if (
Expand Down
6 changes: 5 additions & 1 deletion norminette/rules/check_expression_statement.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from norminette.rules import Rule, Check
from norminette.errors import Error

kw = [
# C reserved keywords #
Expand Down Expand Up @@ -72,7 +73,10 @@ def run(self, context):
context.check_token(tmp, "SEMI_COLON") is False
and context.check_token(tmp, "LPARENTHESIS") is False
):
context.new_error("RETURN_PARENTHESIS", context.peek_token(tmp))
token = context.peek_token(tmp)
error = Error.from_name("RETURN_PARENTHESIS")
error.add_highlight(token)
context.errors.add(error)
return False, 0
elif context.check_token(tmp, "SEMI_COLON") is False:
tmp = context.skip_nest(tmp) + 1
Expand Down
8 changes: 5 additions & 3 deletions norminette/rules/check_func_declaration.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from norminette.rules import Rule, Check
from norminette.errors import Error

types = ["INT", "FLOAT", "CHAR", "DOUBLE", "LONG", "SHORT"]

Expand All @@ -22,7 +23,10 @@ def run(self, context):
arg = 1
while context.check_token(tmp, ["SEMI_COLON", "NEWLINE"]) is False:
if context.check_token(tmp, "LBRACE") is True:
context.new_error("BRACE_NEWLINE", context.peek_token(tmp))
token = context.peek_token(tmp)
error = Error.from_name("BRACE_NEWLINE")
error.add_highlight(*token.pos, length=1)
context.errors.add(error)
tmp += 1
if context.history[-1] == "IsUserDefinedType":
return
Expand Down Expand Up @@ -70,6 +74,4 @@ def run(self, context):
arg = []
while context.check_token(i, ["NEWLINE", "SEMI_COLON"]) is False:
i += 1
if context.check_token(i - 1, ["TAB", "SPACE"]):
context.new_error("SPC_BEFORE_NL", context.peek_token(i))
return False, 0
6 changes: 5 additions & 1 deletion norminette/rules/check_func_spacing.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from norminette.rules import Rule, Check
from norminette.errors import Error

whitespaces = ["SPACE", "TAB", "NEWLINE"]

Expand Down Expand Up @@ -32,7 +33,10 @@ def run(self, context):
while context.check_token(i, ["MULT", "BWISE_AND", "LPARENTHESIS"]) is True:
i -= 1
if context.peek_token(i).type == "SPACE":
context.new_error("SPACE_BEFORE_FUNC", context.peek_token(i))
token = context.peek_token(i)
error = Error.from_name("SPACE_BEFORE_FUNC")
error.add_highlight(*token.pos, length=1, hint="Expected a tab instead of a space")
context.errors.add(error)
return False, 0
if context.peek_token(i).type == "TAB":
j = i
Expand Down
Loading
Loading