stylish.py (6233B)
1 # This Source Code Form is subject to the terms of the Mozilla Public 2 # License, v. 2.0. If a copy of the MPL was not distributed with this 3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5 from mozterm import Terminal 6 7 from ..result import Issue 8 from ..util.string import pluralize 9 10 11 class StylishFormatter: 12 """Formatter based on the eslint default.""" 13 14 _indent_ = " " 15 16 # Colors later on in the list are fallbacks in case the terminal 17 # doesn't support colors earlier in the list. 18 # See http://www.calmar.ws/vim/256-xterm-24bit-rgb-color-chart.html 19 _colors = { 20 "blue": [4], 21 "brightred": [9, 1], 22 "brightyellow": [11, 3], 23 "darkgrey": [247, 8], 24 "green": [2], 25 "grey": [7], 26 "red": [1], 27 "yellow": [3], 28 } 29 30 fmt = """ 31 {c1}{lineno}{column} {c2}{level}{normal} {message} {c1}{rule}({linter}){source}{normal} 32 {diff}""".lstrip("\n") 33 fmt_summary = ( 34 "{t.bold}{c}\u2716 {problem} ({error}, {warning}{failure}, {fixed}){t.normal}" 35 ) 36 37 def __init__(self, disable_colors=False): 38 self.term = Terminal(disable_styling=disable_colors) 39 self.num_colors = self.term.number_of_colors 40 41 def color(self, color): 42 for num in self._colors[color]: 43 if num < self.num_colors: 44 return self.term.color(num) 45 return "" 46 47 def _reset_max(self): 48 self.max_lineno = 0 49 self.max_column = 0 50 self.max_level = 0 51 self.max_message = 0 52 53 def _update_max(self, err): 54 """Calculates the longest length of each token for spacing.""" 55 self.max_lineno = max(self.max_lineno, len(str(err.lineno))) 56 if err.column: 57 self.max_column = max(self.max_column, len(str(err.column))) 58 self.max_level = max(self.max_level, len(str(err.level))) 59 self.max_message = max(self.max_message, len(err.message)) 60 61 def _get_colored_diff(self, diff): 62 if not diff: 63 return "" 64 65 new_diff = "" 66 for line in diff.split("\n"): 67 if line.startswith("+"): 68 new_diff += self.color("green") 69 elif line.startswith("-"): 70 new_diff += self.color("red") 71 else: 72 new_diff += self.term.normal 73 new_diff += self._indent_ + line + "\n" 74 return new_diff 75 76 def _get_colored_source(self, source): 77 if not source: 78 return "" 79 80 new_source = "\n" 81 for line in source.split("\n"): 82 divpos = 0 83 new_source += self._indent_ * 2 84 if "|" in line: 85 new_source += self.color("blue") 86 divpos = line.index("|") + 1 87 new_source += line[:divpos] 88 89 if line[divpos:].strip().startswith("^"): 90 new_source += self.color("red") 91 else: 92 new_source += self.color("grey") 93 94 new_source += line[divpos:] + "\n" 95 return new_source.rstrip("\n") 96 97 def __call__(self, result): 98 message = [] 99 failed = result.failed 100 101 num_errors = 0 102 num_warnings = 0 103 num_fixed = result.fixed 104 for path, errors in sorted(result.issues.items()): 105 self._reset_max() 106 107 message.append(self.term.underline(path)) 108 # Do a first pass to calculate required padding 109 for err in errors: 110 assert isinstance(err, Issue) 111 self._update_max(err) 112 if err.level == "error": 113 num_errors += 1 114 else: 115 num_warnings += 1 116 117 for err in sorted( 118 errors, key=lambda e: (int(e.lineno), int(e.column or 0)) 119 ): 120 if err.column: 121 col = ":" + str(err.column).ljust(self.max_column) 122 else: 123 col = "".ljust(self.max_column + 1) 124 125 args = { 126 "normal": self.term.normal, 127 "c1": self.color("darkgrey"), 128 "c2": ( 129 self.color("red") 130 if err.level == "error" 131 else self.color("yellow") 132 ), 133 "lineno": str(err.lineno).rjust(self.max_lineno), 134 "column": col, 135 "level": err.level.ljust(self.max_level), 136 "rule": f"{err.rule} " if err.rule else "", 137 "linter": err.linter.lower(), 138 "message": err.message.ljust(self.max_message), 139 "diff": self._get_colored_diff(err.diff), 140 "source": self._get_colored_source(err.source), 141 } 142 message.append(self.fmt.format(**args).rstrip().rstrip("\n")) 143 144 message.append("") # newline 145 146 # If there were failures, make it clear which linters failed 147 for fail in failed: 148 message.append( 149 "{c}A failure occurred in the {name} linter.".format( 150 c=self.color("brightred"), name=fail 151 ) 152 ) 153 154 # Print a summary 155 message.append( 156 self.fmt_summary.format( 157 t=self.term, 158 c=( 159 self.color("brightred") 160 if num_errors or failed 161 else self.color("brightyellow") 162 ), 163 problem=pluralize("problem", num_errors + num_warnings + len(failed)), 164 error=pluralize("error", num_errors), 165 warning=pluralize( 166 "warning", num_warnings or result.total_suppressed_warnings 167 ), 168 failure=( 169 ", {}".format(pluralize("failure", len(failed))) if failed else "" 170 ), 171 fixed=f"{num_fixed} fixed", 172 ) 173 ) 174 175 if result.total_suppressed_warnings > 0 and num_errors == 0: 176 message.append( 177 "(pass {c1}-W/--warnings{c2} to see warnings.)".format( 178 c1=self.color("darkgrey"), c2=self.term.normal 179 ) 180 ) 181 182 return "\n".join(message)