File size: 5,597 Bytes
f770010
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
from contextlib import contextmanager
from typing import Dict, List


class _NormalizerMeta(type):
    def __new__(cls, name, bases, dct):
        new_cls = type.__new__(cls, name, bases, dct)
        new_cls.rule_value_classes = {}
        new_cls.rule_type_classes = {}
        return new_cls


class Normalizer(metaclass=_NormalizerMeta):
    _rule_type_instances: Dict[str, List[type]] = {}
    _rule_value_instances: Dict[str, List[type]] = {}

    def __init__(self, grammar, config):
        self.grammar = grammar
        self._config = config
        self.issues = []

        self._rule_type_instances = self._instantiate_rules('rule_type_classes')
        self._rule_value_instances = self._instantiate_rules('rule_value_classes')

    def _instantiate_rules(self, attr):
        dct = {}
        for base in type(self).mro():
            rules_map = getattr(base, attr, {})
            for type_, rule_classes in rules_map.items():
                new = [rule_cls(self) for rule_cls in rule_classes]
                dct.setdefault(type_, []).extend(new)
        return dct

    def walk(self, node):
        self.initialize(node)
        value = self.visit(node)
        self.finalize()
        return value

    def visit(self, node):
        try:
            children = node.children
        except AttributeError:
            return self.visit_leaf(node)
        else:
            with self.visit_node(node):
                return ''.join(self.visit(child) for child in children)

    @contextmanager
    def visit_node(self, node):
        self._check_type_rules(node)
        yield

    def _check_type_rules(self, node):
        for rule in self._rule_type_instances.get(node.type, []):
            rule.feed_node(node)

    def visit_leaf(self, leaf):
        self._check_type_rules(leaf)

        for rule in self._rule_value_instances.get(leaf.value, []):
            rule.feed_node(leaf)

        return leaf.prefix + leaf.value

    def initialize(self, node):
        pass

    def finalize(self):
        pass

    def add_issue(self, node, code, message):
        issue = Issue(node, code, message)
        if issue not in self.issues:
            self.issues.append(issue)
        return True

    @classmethod
    def register_rule(cls, *, value=None, values=(), type=None, types=()):
        """
        Use it as a class decorator::

            normalizer = Normalizer('grammar', 'config')
            @normalizer.register_rule(value='foo')
            class MyRule(Rule):
                error_code = 42
        """
        values = list(values)
        types = list(types)
        if value is not None:
            values.append(value)
        if type is not None:
            types.append(type)

        if not values and not types:
            raise ValueError("You must register at least something.")

        def decorator(rule_cls):
            for v in values:
                cls.rule_value_classes.setdefault(v, []).append(rule_cls)
            for t in types:
                cls.rule_type_classes.setdefault(t, []).append(rule_cls)
            return rule_cls

        return decorator


class NormalizerConfig:
    normalizer_class = Normalizer

    def create_normalizer(self, grammar):
        if self.normalizer_class is None:
            return None

        return self.normalizer_class(grammar, self)


class Issue:
    def __init__(self, node, code, message):
        self.code = code
        """
        An integer code that stands for the type of error.
        """
        self.message = message
        """
        A message (string) for the issue.
        """
        self.start_pos = node.start_pos
        """
        The start position position of the error as a tuple (line, column). As
        always in |parso| the first line is 1 and the first column 0.
        """
        self.end_pos = node.end_pos

    def __eq__(self, other):
        return self.start_pos == other.start_pos and self.code == other.code

    def __ne__(self, other):
        return not self.__eq__(other)

    def __hash__(self):
        return hash((self.code, self.start_pos))

    def __repr__(self):
        return '<%s: %s>' % (self.__class__.__name__, self.code)


class Rule:
    code: int
    message: str

    def __init__(self, normalizer):
        self._normalizer = normalizer

    def is_issue(self, node):
        raise NotImplementedError()

    def get_node(self, node):
        return node

    def _get_message(self, message, node):
        if message is None:
            message = self.message
            if message is None:
                raise ValueError("The message on the class is not set.")
        return message

    def add_issue(self, node, code=None, message=None):
        if code is None:
            code = self.code
            if code is None:
                raise ValueError("The error code on the class is not set.")

        message = self._get_message(message, node)

        self._normalizer.add_issue(node, code, message)

    def feed_node(self, node):
        if self.is_issue(node):
            issue_node = self.get_node(node)
            self.add_issue(issue_node)


class RefactoringNormalizer(Normalizer):
    def __init__(self, node_to_str_map):
        self._node_to_str_map = node_to_str_map

    def visit(self, node):
        try:
            return self._node_to_str_map[node]
        except KeyError:
            return super().visit(node)

    def visit_leaf(self, leaf):
        try:
            return self._node_to_str_map[leaf]
        except KeyError:
            return super().visit_leaf(leaf)