tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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)