439 lines
12 KiB
Python
439 lines
12 KiB
Python
"""Classes for representing and interacting with Tcl syntax trees. """
|
|
|
|
|
|
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)
|
|
|
|
|
|
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):
|
|
def accept(self, visitor, recurse=False):
|
|
if recurse:
|
|
self._recurse(visitor)
|
|
visitor.visit_comment(self)
|
|
|
|
|
|
class Command(Node):
|
|
def __init__(self, routine, *args, 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):
|
|
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
|
|
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, 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)
|
|
|
|
|
|
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)
|