"""Classes for representing and interacting with Tcl syntax trees.""" from __future__ import annotations class Visitor: """Abstract base class for Visitors that operate on syntax tree.""" def visit_script(self, script): pass def visit_comment(self, comment): pass def visit_command(self, command): pass def visit_command_sub(self, command_sub): pass def visit_bare_word(self, word): pass def visit_braced_word(self, word): pass def visit_quoted_word(self, word): pass def visit_compound_bare_word(self, word): pass def visit_var_sub(self, var_sub): pass def visit_arg_expansion(self, arg_expansion): pass def visit_list(self, list): pass def visit_expression(self, expression): pass def visit_braced_expression(self, expression): pass def visit_paren_expression(self, expression): pass def visit_unary_op(self, unary_op): pass def visit_binary_op(self, binary_op): pass def visit_ternary_op(self, ternary_op): pass def visit_function(self, function): pass class Node: """ Invariants: - self.value is some sort of base Python type - self.children is a list of Node types """ def __init__(self, *init, pos=None, end_pos=None): """pos: line, column of first character of parsed region (1-indexed) end_pos: line, column of first character after parsed region (1-indexed) """ self.line = None self.col = None if pos is not None: self.line, self.col = pos self.end_pos = end_pos self.value = None if len(init) > 0 and not isinstance(init[0], Node): self.value = init[0] init = init[1:] if not all(isinstance(v, Node) for v in init): raise TypeError("Children must be Node instances") self.children = list(init) def add(self, node): self.children.append(node) @property def contents(self): """This is overloaded by word Nodes that may have concrete contents. TODO: I prefer the name value, but that's currently taken... """ return None @property def contents_pos(self): """This is overloaded by word Nodes that may have concrete contents. Returns the position at which the contents start. TODO: consider combining with with `contents`? """ return None def _pos_str(self): start_pos_str = "?" if self.pos is not None: start_pos_str = f"{self.pos[0]}:{self.pos[1]}" end_pos_str = "?" if self.end_pos is not None: end_pos_str = f"{self.end_pos[0]}:{self.end_pos[1]}" return f" # {start_pos_str}-{end_pos_str}" def _make_str(self, indent=None, positions=False): if indent is not None: s = " " * indent else: s = "" s += self.__class__.__name__ s += "(" if self.value: s += repr(self.value) if self.children: s += ", " if positions and self.children: s += self._pos_str() for i, child in enumerate(self.children): if indent is not None: s += "\n" s += child._make_str( indent=None if indent is None else indent + 1, positions=positions ) if i < len(self.children) - 1: s += ", " s += ")" if positions and not self.children: s += self._pos_str() return s def pretty(self, positions=False): return self._make_str(indent=0, positions=positions) def __str__(self): return self._make_str() def __eq__(self, other): if type(self) is not type(other): return False if self.value != other.value: return False if len(self.children) != len(other.children): return False for my_child, other_child in zip(self.children, other.children): if my_child != other_child: return False return True def diff(self, other, indent_depth=0): lines = [] indent = " " * indent_depth my_cls = self.__class__.__name__ other_cls = other.__class__.__name__ if my_cls != other_cls: lines += [f"{indent}-{my_cls}("] lines += [f"{indent}+{other_cls}("] return lines if self.value != other.value: lines += [f'{indent}-{my_cls}("{self.value}"'] lines += [f'{indent}+{other_cls}("{other.value}"'] return lines if len(self.children) != len(other.children): my_children = ",".join([ child.__class__.__name__ for child in self.children ]) other_children = ",".join([ child.__class__.__name__ for child in other.children ]) lines += [f"{indent}-{my_cls}({my_children})"] lines += [f"{indent}+{other_cls}({other_children})"] return lines if self.value is not None: lines += [f"{indent}{my_cls}({self.value}"] else: lines += [f"{indent}{my_cls}("] for my_child, other_child in zip(self.children, other.children): lines += my_child.diff(other_child, indent_depth=indent_depth + 1) lines += [f"{indent})"] return lines @property def pos(self): if self.line is None or self.col is None: return None return (self.line, self.col) def _recurse(self, visitor): for child in self.children: child.accept(visitor, recurse=True) def _pos_match(self, line: int, col: int) -> bool: """Return True if pos is within this node's block""" if self.pos is None: return False if self.end_pos is None: return line == self.pos[0] and col == self.pos[1] return ( line >= self.pos[0] and line <= self.end_pos[0] and (col >= self.pos[1] or line > self.pos[0]) and (col < self.end_pos[1] or line < self.end_pos[0]) ) def find_by_pos(self, line: int, col: int) -> Node | None: """Find the deepest child node in the tree (i.e. most granular match) that matches the given position.""" if not self._pos_match(line, col): return None for child in self.children: if child._pos_match(line, col): return child.find_by_pos(line, col) return self class Script(Node): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # hack for spaces-in-braces check self.braced = False def accept(self, visitor, recurse=False): if recurse: self._recurse(visitor) visitor.visit_script(self) class Comment(Node): value: str def __init__(self, value: str, pos=None, end_pos=None): super().__init__(value, pos=pos, end_pos=end_pos) def accept(self, visitor, recurse=False): if recurse: self._recurse(visitor) visitor.visit_comment(self) class Command(Node): def __init__(self, routine: Node, *args: Node, pos=None, end_pos=None): self.routine = routine self.args = args super().__init__(routine, *args, pos=pos, end_pos=end_pos) def accept(self, visitor, recurse=False): if recurse: self._recurse(visitor) visitor.visit_command(self) class CommandSub(Node): def accept(self, visitor, recurse=False): if recurse: self._recurse(visitor) visitor.visit_command_sub(self) class BareWord(Node): def accept(self, visitor, recurse=False): if recurse: self._recurse(visitor) visitor.visit_bare_word(self) @property def contents(self): return self.value @property def contents_pos(self): return self.pos class BracedWord(Node): def accept(self, visitor, recurse=False): if recurse: self._recurse(visitor) visitor.visit_braced_word(self) @property def contents(self): return self.value @property def contents_pos(self): if self.line is None or self.col is None: return None return (self.line, self.col + 1) class QuotedWord(Node): def accept(self, visitor, recurse=False): if recurse: self._recurse(visitor) visitor.visit_quoted_word(self) @property def contents(self): """A QuotedWord with concrete contents is put in the tree as QuotedWord(BareWord()), so return contents of its child.""" if len(self.children) > 1: return None if len(self.children) == 0: # weird special case to handle blank quoted string ("") - it doesn't # make sense to put in a child, but setting/returning the value is # also inconsistent return "" return self.children[0].contents @property def contents_pos(self): if self.contents is None: return None if self.line is None or self.col is None: return None return (self.line, self.col + 1) class CompoundBareWord(Node): def accept(self, visitor, recurse=False): if recurse: self._recurse(visitor) visitor.visit_compound_bare_word(self) class VarSub(Node): def __init__(self, *args, braced=False, **kwargs): self.braced = braced return super().__init__(*args, **kwargs) def accept(self, visitor, recurse=False): if recurse: self._recurse(visitor) visitor.visit_var_sub(self) class ArgExpansion(Node): def __init__(self, list: Node, pos=None, end_pos=None): self.list = list super().__init__(list, pos=pos, end_pos=end_pos) def accept(self, visitor, recurse=False): if recurse: self._recurse(visitor) visitor.visit_arg_expansion(self) @property def contents(self): return self.list.contents class List(Node): """This Node currently exists exclusively for implementing the switch command in a way that facilitates style checks. Might be nice to find another way to handle this that doesn't require a special Node.""" def accept(self, visitor, recurse=False): if recurse: self._recurse(visitor) visitor.visit_list(self) class Expression(Node): def accept(self, visitor, recurse=False): if recurse: self._recurse(visitor) visitor.visit_expression(self) class BracedExpression(Node): def accept(self, visitor, recurse=False): if recurse: self._recurse(visitor) visitor.visit_braced_expression(self) class ParenExpression(Node): def __init__(self, body: Expression, pos=None, end_pos=None): self.body = body super().__init__(body, pos=pos, end_pos=end_pos) def accept(self, visitor, recurse=False): if recurse: self._recurse(visitor) visitor.visit_paren_expression(self) class UnaryOp(Node): def __init__(self, operator, operand, pos=None, end_pos=None): self.operator = operator self.operand = operand super().__init__(operator, operand, pos=pos, end_pos=end_pos) def accept(self, visitor, recurse=False): if recurse: self._recurse(visitor) visitor.visit_unary_op(self) class BinaryOp(Node): def __init__(self, operator1, operand, operator2, pos=None, end_pos=None): self.operator1 = operator1 self.operand = operand self.operator2 = operator2 super().__init__(operator1, operand, operator2, pos=pos, end_pos=end_pos) def accept(self, visitor, recurse=False): if recurse: self._recurse(visitor) visitor.visit_binary_op(self) class TernaryOp(Node): def __init__(self, condition, question, true, colon, false, pos=None, end_pos=None): self.condition = condition self.question = question self.true = true self.colon = colon self.false = false super().__init__( condition, question, true, colon, false, pos=pos, end_pos=end_pos ) def accept(self, visitor, recurse=False): if recurse: self._recurse(visitor) visitor.visit_ternary_op(self) class Function(Node): def __init__(self, name, *args, pos=None, end_pos=None): self.name = name self.args = args super().__init__(name, *args, pos=pos, end_pos=end_pos) def accept(self, visitor, recurse=False): if recurse: self._recurse(visitor) visitor.visit_function(self)