summaryrefslogtreecommitdiff
path: root/scss/errors.py
blob: c898500dface7bb36a8cb7e2e917bf4bce98dcf4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import sys
import traceback


def add_error_marker(text, position, start_line=1):
    """Add a caret marking a given position in a string of input.

    Returns (new_text, caret_line).
    """
    indent = "    "
    lines = []
    caret_line = start_line
    for line in text.split("\n"):
        lines.append(indent + line)

        if 0 <= position <= len(line):
            lines.append(indent + (" " * position) + "^")
            caret_line = start_line

        position -= len(line)
        position -= 1  # for the newline
        start_line += 1

    return "\n".join(lines), caret_line


class SassError(Exception):
    """Error class that wraps another exception and attempts to bolt on some
    useful context.
    """
    def __init__(self, exc, rule=None, expression=None, expression_pos=None):
        self.exc = exc

        self.rule_stack = []
        if rule:
            self.rule_stack.append(rule)

        self.expression = expression
        self.expression_pos = expression_pos

        _, _, self.original_traceback = sys.exc_info()

    def add_rule(self, rule):
        """Add a new rule to the "stack" of rules -- this is used to track,
        e.g., how a file was ultimately imported.
        """
        self.rule_stack.append(rule)

    def format_prefix(self):
        # TODO this contains NULs and line numbers; could be much prettier
        if self.rule_stack:
            return "Error parsing block:\n" + "    " + self.rule_stack[0].unparsed_contents
        else:
            return "Unknown error"

    def __str__(self):
        prefix = self.format_prefix()

        ret = [prefix, "\n\n"]

        if self.rule_stack:
            # TODO this looks very neat-o but doesn't actually work because
            # manage_children doesn't recurse for imports  :)
            ret.extend(("From ", self.rule_stack[0].file_and_line, "\n"))
            last_file = self.rule_stack[0].source_file

            for rule in self.rule_stack[1:]:
                if rule.source_file is not last_file:
                    ret.extend(("...imported from ", rule.file_and_line, "\n"))
                last_file = rule.source_file

            ret.append("\n")

        ret.append("Traceback:\n")
        ret.extend(traceback.format_tb(self.original_traceback))
        ret.extend((type(self.exc).__name__, ": ", str(self.exc), "\n"))
        return ''.join(ret)


class SassParseError(SassError):
    """Error raised when parsing a Sass expression fails."""

    def format_prefix(self):
        decorated_expr, line = add_error_marker(self.expression, self.expression_pos or -1)
        return """Error parsing expression:\n""" + decorated_expr


class SassEvaluationError(SassError):
    """Error raised when evaluating a parsed expression fails."""

    def format_prefix(self):
        # TODO would be nice for the AST to have position information
        # TODO might be nice to print the AST and indicate where the failure
        # was?
        decorated_expr, line = add_error_marker(self.expression, self.expression_pos or -1)
        return """Error evaluating expression:\n""" + decorated_expr