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)