Skip to content

Commit f81be75

Browse files
committed
add Frame class to render errors in HumanizedErrorsFormatter
1 parent dc21fd3 commit f81be75

File tree

1 file changed

+127
-15
lines changed

1 file changed

+127
-15
lines changed

norminette/errors.py

Lines changed: 127 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import os
44
import json
5+
import collections
56
from dataclasses import dataclass, field, asdict
67
from typing import (
78
TYPE_CHECKING,
@@ -37,8 +38,8 @@ def __lt__(self, other: Any) -> bool:
3738
if self.lineno == other.lineno:
3839
if self.column == other.column:
3940
return len(self.hint or '') > len(other.hint or '')
40-
return self.column > other.column
41-
return self.lineno > other.lineno
41+
return self.column < other.column
42+
return self.lineno < other.lineno
4243

4344
@classmethod
4445
def from_token(
@@ -54,6 +55,53 @@ def from_token(
5455
hint=hint,
5556
)
5657

58+
@staticmethod
59+
def merge(highlights: list[Highlight]) -> list[Highlight]:
60+
if len(highlights) < 2:
61+
return highlights
62+
63+
highlights.sort()
64+
65+
drop = []
66+
67+
last = None
68+
for index, highlight in enumerate(highlights):
69+
if last is None:
70+
last = highlight
71+
continue
72+
73+
last_len = last.length or 0
74+
curr_len = highlight.length or 0
75+
if (last.column + last_len) >= highlight.column and not last.hint and not highlight.hint:
76+
last.length = last_len + curr_len
77+
drop.append(index)
78+
else:
79+
last = highlight
80+
81+
for index in reversed(drop):
82+
del highlights[index]
83+
84+
return highlights
85+
86+
@staticmethod
87+
def unpack(highlights: list[Highlight]):
88+
result = []
89+
90+
by_line = collections.defaultdict(list)
91+
for highlight in highlights:
92+
by_line[highlight.lineno].append(highlight)
93+
for lineno, hls in by_line.items():
94+
rest = []
95+
for highlight in hls:
96+
if highlight.hint:
97+
result.append((lineno, [highlight]))
98+
else:
99+
rest.append(highlight)
100+
if rest:
101+
result.append((lineno, rest))
102+
103+
return result
104+
57105

58106
@dataclass
59107
class Error:
@@ -204,24 +252,88 @@ def _colorize_error_text(self, error: Error) -> str:
204252
return f"\x1b[{color}m{error.text}\x1b[0m"
205253

206254

255+
class Frame:
256+
__slots__ = "file", "error", "colorize"
257+
258+
def __init__(self, file: File, error: Error, *, colorize: bool = False) -> None:
259+
self.file = file
260+
self.error = error
261+
self.colorize = colorize
262+
263+
@property
264+
def _path(self) -> str:
265+
items: list[str | int] = [self.file.path]
266+
for highlight in self.error.highlights:
267+
items.append(highlight.lineno)
268+
items.append(highlight.column)
269+
break
270+
path = ':'.join(map(str, items))
271+
return path if not self.colorize else f"\x1b[;97m{path}\x1b[0m"
272+
273+
def _build_code_sublines(self, highlights: list[Highlight]):
274+
subline = ''
275+
hint = ''
276+
for highlight in highlights:
277+
if highlight.hint:
278+
if hint:
279+
hint += ', '
280+
hint += highlight.hint
281+
if len(subline) < highlight.column - 1:
282+
subline += ' ' * (highlight.column - 1 - len(subline))
283+
subline += '^' * (highlight.length or 1)
284+
if hint:
285+
subline += f" \x1b[3;94m{hint}\x1b[0m"
286+
yield subline
287+
288+
def _build_code_lines(self, lineno: int, arrows: List[Highlight]):
289+
assert arrows, "No highlights to build line from"
290+
291+
arrows = Highlight.merge(arrows)
292+
arrows = Highlight.unpack(arrows)
293+
print("Unpacked", arrows)
294+
295+
for lineno, highlights in arrows: # type: ignore
296+
yield f" {lineno:>5} | {self.file[lineno,].translated}"
297+
298+
for subline in self._build_code_sublines(highlights):
299+
yield f" {' ':>5} | \x1b[;91m{subline}\x1b[0m"
300+
301+
def __str__(self):
302+
lines = []
303+
lines.append(self._path + f" {self.error.name}")
304+
305+
last = None
306+
arrows = []
307+
for highlight in sorted(self.error.highlights):
308+
if highlight.length is None:
309+
continue
310+
if last and last.lineno == highlight.lineno:
311+
arrows.append(highlight)
312+
else:
313+
if last:
314+
code = self._build_code_lines(last.lineno, arrows)
315+
lines.extend(code)
316+
arrows = [highlight]
317+
last = highlight
318+
if last:
319+
code = self._build_code_lines(last.lineno, arrows)
320+
lines.extend(code)
321+
322+
if len(lines) == 1:
323+
lines[0] += f" {self.error.text}"
324+
else:
325+
lines[0] += f" \x1b[0;91m{self.error.text}\x1b[0m"
326+
327+
return '\n'.join(lines)
328+
329+
207330
class HumanizedErrorsFormatter(_formatter):
208331
def __str__(self) -> str:
209332
output = ''
210333
for file in self.files:
211334
for error in file.errors:
212-
highlight = error.highlights[0]
213-
# Location
214-
output += f"\x1b[;97m{file.path}:{highlight.lineno}:{highlight.column}\x1b[0m"
215-
output += ' ' + error.name
216-
if not highlight.length:
217-
output += ' ' + error.text
218-
if highlight.length:
219-
# Line
220-
output += f"\n {highlight.lineno:>5} | {file[highlight.lineno,].translated}"
221-
# Arrow
222-
output += "\n | " + ' ' * (highlight.column - 1)
223-
output += f"\x1b[0;91m{'^' * (highlight.length or 0)} {highlight.hint or error.text}\x1b[0m"
224-
output += '\n'
335+
frame = Frame(file, error, colorize=self.use_colors)
336+
output += str(frame) + '\n'
225337
return output
226338

227339

0 commit comments

Comments
 (0)