import io import re from typing import Optional, Tuple from tclint.parser import Parser from tclint.commands import CommandArgError from tclint.commands.checks import eval as eval_script_args from tclint.syntax_tree import ( BracedWord, BareWord, BracedExpression, List, QuotedWord, Expression, ) from tclint.lexer import TclSyntaxError, Lexer, TOK_EOF _UPLEVEL_LEVEL_RE = re.compile(r"^#?\d+$") def _uplevel(args, parser): """uplevel ?level? arg ?arg ...?""" # ref: https://www.tcl.tk/man/tcl/TclCmd/uplevel.html if len(args) == 0: raise CommandArgError("not enough args to 'uplevel': got 0, expected at least 1") # The level can only be omitted when the first arg doesn't look like one. # A non-literal first arg (e.g. $level) is treated as a level as well. level = [] if len(args) > 1: first = args[0].contents if first is None or _UPLEVEL_LEVEL_RE.match(first): level = args[0:1] return level + eval_script_args(args[len(level) :], parser, "uplevel") class CustomParser(Parser): def __init__(self, debug=False, command_plugins=None): super().__init__(debug, command_plugins) # tclint only checks the arg count of uplevel; parse its body as a script # so it gets formatted and linted like eval/namespace eval bodies. self._commands = {**self._commands, "uplevel": _uplevel} # Used to normalize newlines consistently with open()'s universal newlines mode. self._decoder = io.IncrementalNewlineDecoder(None, True) def parse(self, script: str, pos: Optional[Tuple[int, int]] = None): script = self._decoder.decode(script, True) lexer = Lexer(pos=pos) lexer.input(script) tree = self._parse_script(lexer, in_command_sub=False) assert lexer.type() == TOK_EOF, ( "Didn't reach EOF parsing script, please file a bug report." ) return tree def _parse_operator(self, ts): pos = ts.pos() # hacky logic to handle parsing legal operators if ts.value() in {"*", "&", "|"}: # one or two of these characters are legal operators operator = ts.value() ts.next() if ts.value() == operator: operator += ts.value() ts.next() elif ts.value() in {"<", ">"}: operator = ts.value() ts.next() if ts.value() in {operator, "="}: operator += ts.value() ts.next() elif ts.value() in {"=", "!"}: operator = ts.value() ts.next() if ts.value() != "=": raise TclSyntaxError( f"invalid operator in expression: {operator}", pos, ts.pos() ) operator += ts.value() ts.next() elif ts.value() in {"*", "/", "%", "+", "-", "^", "eq", "ne", "in", "ni"}: operator = ts.value() ts.next() else: message = "invalid operator in expression: " if ts.value() == "\\ ": message += ( "\\ (check for trailing whitespace if it's the end of the line)" ) else: message += ts.value() raise TclSyntaxError(message, pos, ts.pos()) return BareWord(operator, pos=pos, end_pos=ts.pos())