99 lines
3.3 KiB
Python
99 lines
3.3 KiB
Python
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())
|