22
33import os
44import json
5+ import collections
56from dataclasses import dataclass , field , asdict
67from 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
59107class 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+
207330class 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