add updates libs
This commit is contained in:
@@ -1,25 +1,45 @@
|
||||
"""Helpers for checking command arguments."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import List, Optional, Union
|
||||
from __future__ import annotations
|
||||
|
||||
from tclint.syntax_tree import ArgExpansion, QuotedWord, BracedWord, BareWord, Node
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from tclint.syntax_tree import ArgExpansion, BareWord, BracedWord, Node, QuotedWord
|
||||
|
||||
# This lets us use Parser in type annotations without introducing a cyclic dependency.
|
||||
if TYPE_CHECKING:
|
||||
from tclint.parser import Parser
|
||||
|
||||
|
||||
class CommandArgError(Exception):
|
||||
"""Exception raised by command handlers to indicate invalid arguments."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def arg_count(args, parser):
|
||||
# TODO: graceful handling of argsub going into things with recursive parsing.
|
||||
# if the argsub happens to be "concrete", we can technically do the right
|
||||
# thing (although this should probably be flagged as a readability issue...)
|
||||
# otherwise, we should flag that the non-concrete argsub is not okay for
|
||||
# these cases. however, I think its not okay-ness doesn't need to be absolute, e.g.
|
||||
# I think we could allow:
|
||||
#
|
||||
# catch {puts "my script"} {*}$catchopts
|
||||
#
|
||||
def arg_count(args: list[Node], parser: Parser) -> tuple[int, bool]:
|
||||
"""Returns the number of arguments in args, taking {*} into account.
|
||||
|
||||
If an argument list contains an argument expansion operator that cannot be
|
||||
statically expanded, the second return value is True, and the count is the minimum
|
||||
possible number of arguments. Otherwise, the return value is False and the count
|
||||
reflects the exact number of arguments.
|
||||
|
||||
This function should always be used for validating argument count, rather than
|
||||
relying on `len(args)`.
|
||||
"""
|
||||
|
||||
# TODO: Replace this with or add a similar `expand_args` function that returns an
|
||||
# expanded argument list. One tricky thing is we want to handle cases like:
|
||||
# - foreach {*}$iters { ...body... }
|
||||
# - catch {puts "my script"} {*}$catchopts
|
||||
# With something like a list structure we can either forward or reverse index to
|
||||
# still parse the body even with an unexpanded {*}.
|
||||
|
||||
# TODO: Add a violation that flags `{*}{a b c}` for rewriting as `a b c`. A future
|
||||
# version of tclfmt that allows rewrites that break the syntax tree could do this
|
||||
# automatically.
|
||||
|
||||
arg_count = 0
|
||||
has_arg_expansion = False
|
||||
@@ -28,14 +48,14 @@ def arg_count(args, parser):
|
||||
if arg.contents is None:
|
||||
has_arg_expansion = True
|
||||
continue
|
||||
arg_count += len(parser.parse_list(arg.contents))
|
||||
arg_count += len(parser.parse_list(arg).children)
|
||||
else:
|
||||
arg_count += 1
|
||||
|
||||
return arg_count, has_arg_expansion
|
||||
|
||||
|
||||
def check_count(command, min=None, max=None, args_name="args"):
|
||||
def check_count(command, min=None, max=None):
|
||||
def check(args, parser):
|
||||
if min is None and max is None:
|
||||
return None
|
||||
@@ -44,19 +64,17 @@ def check_count(command, min=None, max=None, args_name="args"):
|
||||
|
||||
if not has_arg_expansion and min == max and count != min:
|
||||
raise CommandArgError(
|
||||
f"wrong # of {args_name} for {command}: got {count}, expected {min}"
|
||||
f"wrong # of args for {command}: got {count}, expected {min}"
|
||||
)
|
||||
|
||||
if not has_arg_expansion and min is not None and count < min:
|
||||
raise CommandArgError(
|
||||
f"not enough {args_name} for {command}: got {count}, expected at least"
|
||||
f" {min}"
|
||||
f"not enough args for {command}: got {count}, expected at least {min}"
|
||||
)
|
||||
|
||||
if max is not None and count > max:
|
||||
raise CommandArgError(
|
||||
f"too many {args_name} for {command}: got {count}, expected no more"
|
||||
f" than {max}"
|
||||
f"too many args for {command}: got {count}, expected no more than {max}"
|
||||
)
|
||||
|
||||
return None
|
||||
@@ -64,7 +82,7 @@ def check_count(command, min=None, max=None, args_name="args"):
|
||||
return check
|
||||
|
||||
|
||||
def eval(args, parser, command):
|
||||
def eval(args: list[Node], parser: Parser, command: str) -> list[Node]:
|
||||
if len(args) > 1 and any(isinstance(arg, (QuotedWord, BracedWord)) for arg in args):
|
||||
# Slightly odd restriction, but our syntax tree doesn't have a great way
|
||||
# to handle this case. We require each command argument to correspond to
|
||||
@@ -106,15 +124,18 @@ def eval(args, parser, command):
|
||||
|
||||
prev_arg_end_pos = arg.end_pos
|
||||
|
||||
script = parser.parse(eval_script, pos=(args[0].pos))
|
||||
script = parser.parse(eval_script, pos=(args[0].contents_pos))
|
||||
script.end_pos = args[-1].end_pos
|
||||
|
||||
return [script]
|
||||
|
||||
|
||||
def check_command(
|
||||
command: str, args: List[Node], parser, command_spec: Union[Callable, dict, None]
|
||||
) -> Optional[List[Node]]:
|
||||
command: str,
|
||||
args: list[Node],
|
||||
parser: Parser,
|
||||
command_spec: Callable | dict | None,
|
||||
) -> Optional[list[Node]]:
|
||||
if command_spec is None:
|
||||
return None
|
||||
|
||||
@@ -124,42 +145,97 @@ def check_command(
|
||||
return command_spec(args, parser)
|
||||
|
||||
|
||||
def _positional_has_type(type: str, arg_spec: dict, indices: list[int]) -> bool:
|
||||
return any([arg_spec["positionals"][i]["value"]["type"] == type for i in indices])
|
||||
|
||||
|
||||
def check_arg_spec(
|
||||
command: str, args: List[Node], parser, arg_spec: dict
|
||||
) -> Optional[List[Node]]:
|
||||
command: str, args: list[Node], parser: Parser, arg_spec: dict
|
||||
) -> Optional[list[Node]]:
|
||||
if "subcommands" in arg_spec:
|
||||
subcommands = arg_spec["subcommands"]
|
||||
try:
|
||||
subcommand = args[0].contents
|
||||
except IndexError:
|
||||
subcommand = None
|
||||
|
||||
if subcommand in subcommands:
|
||||
new_args = check_command(
|
||||
f"{command} {subcommand}", args[1:], parser, subcommands[subcommand]
|
||||
)
|
||||
if new_args is None:
|
||||
return new_args
|
||||
return args[0:1] + new_args
|
||||
|
||||
if "" in subcommands:
|
||||
return check_command(command, args, parser, subcommands[""])
|
||||
|
||||
if subcommand is not None:
|
||||
msg = f"invalid subcommand for {command}: got {subcommand}"
|
||||
else:
|
||||
msg = f"no subcommand provided for {command}"
|
||||
|
||||
raise CommandArgError(f"{msg}, expected one of {', '.join(subcommands.keys())}")
|
||||
return dispatch_subcommands(command, args, parser, arg_spec["subcommands"])
|
||||
|
||||
switches = arg_spec["switches"]
|
||||
args_allowed = set(switches)
|
||||
mapped, positional_args = map_switches(args, switches, command)
|
||||
|
||||
args_required = {switch for switch in switches if switches[switch]["required"]}
|
||||
missing_required = args_required.difference(mapped)
|
||||
|
||||
if len(missing_required) > 1:
|
||||
raise CommandArgError(
|
||||
f"missing required arguments for {command}: {', '.join(missing_required)}"
|
||||
)
|
||||
elif len(missing_required) == 1:
|
||||
raise CommandArgError(
|
||||
f"missing required argument for {command}: {missing_required.pop()}"
|
||||
)
|
||||
|
||||
positionals = [args[i] for i in positional_args]
|
||||
mapping = map_positionals(positionals, arg_spec["positionals"], command)
|
||||
args = list(args)
|
||||
for arg_i, map_to_spec in zip(positional_args, mapping):
|
||||
if _positional_has_type("script", arg_spec, map_to_spec):
|
||||
args[arg_i] = parser.parse_script(args[arg_i])
|
||||
elif _positional_has_type("expression", arg_spec, map_to_spec):
|
||||
args[arg_i] = parser.parse_expression(args[arg_i])
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def dispatch_subcommands(
|
||||
command: str, args: list[Node], parser: Parser, spec: dict
|
||||
) -> Optional[list[Node]]:
|
||||
try:
|
||||
subcommand = args[0].contents
|
||||
except IndexError:
|
||||
subcommand = None
|
||||
|
||||
if subcommand in spec:
|
||||
new_args = check_command(
|
||||
f"{command} {subcommand}", args[1:], parser, spec[subcommand]
|
||||
)
|
||||
if new_args is None:
|
||||
return new_args
|
||||
return args[0:1] + new_args
|
||||
|
||||
if "" in spec:
|
||||
return check_command(command, args, parser, spec[""])
|
||||
|
||||
if subcommand is not None:
|
||||
msg = f"invalid subcommand for {command}: got {subcommand}"
|
||||
else:
|
||||
msg = f"no subcommand provided for {command}"
|
||||
|
||||
raise CommandArgError(f"{msg}, expected one of {', '.join(spec.keys())}")
|
||||
|
||||
|
||||
def map_switches(
|
||||
args: list[Node], switches: dict, command_name: str
|
||||
) -> tuple[set[str], list[int]]:
|
||||
"""Separates switch arguments from positional arguments in a command's argument
|
||||
list.
|
||||
|
||||
`switches` represents the "switches" entry of the spec for the given command. The
|
||||
return value is a tuple of (mapped_switches, positional_indices). mapped_switches is
|
||||
a set of switch names that were found in args. positional_indices is a list of
|
||||
indices into args for arguments that are not switches.
|
||||
|
||||
If the switches found do not map correctly to the spec, this function raises
|
||||
CommandArgError.
|
||||
|
||||
The `command_name` argument is used to generate descriptive error messages.
|
||||
"""
|
||||
mapped: set[str] = set()
|
||||
|
||||
if not switches:
|
||||
return mapped, list(range(len(args)))
|
||||
|
||||
positional_args = []
|
||||
|
||||
args = list(args)
|
||||
while len(args) > 0:
|
||||
arg = args.pop(0)
|
||||
arg_i = 0
|
||||
while arg_i < len(args):
|
||||
arg = args[arg_i]
|
||||
arg_i += 1
|
||||
|
||||
# To facilitate better error messages, we expect that switches are always
|
||||
# specified as BareWords that start with "-" or ">". This lets us throw an
|
||||
@@ -170,71 +246,146 @@ def check_arg_spec(
|
||||
# any switches should be BareWords.
|
||||
contents = arg.contents
|
||||
if not (isinstance(arg, BareWord) and contents and contents[0] in {"-", ">"}):
|
||||
positional_args.append(arg)
|
||||
positional_args.append(arg_i - 1)
|
||||
continue
|
||||
|
||||
# TODO check required arguments
|
||||
if contents in args_allowed:
|
||||
if contents in switches:
|
||||
if contents in mapped and not switches[contents]["repeated"]:
|
||||
raise CommandArgError(
|
||||
f"duplicate argument for {command_name}: {contents}"
|
||||
)
|
||||
if switches[contents]["value"]:
|
||||
try:
|
||||
args.pop(0)
|
||||
except IndexError:
|
||||
arg_i += 1
|
||||
if arg_i > len(args):
|
||||
raise CommandArgError(
|
||||
f"invalid arguments for {command}: expected value after"
|
||||
f"invalid arguments for {command_name}: expected value after"
|
||||
f" {contents}"
|
||||
)
|
||||
if not switches[contents]["repeated"]:
|
||||
args_allowed.remove(contents)
|
||||
if contents in args_required:
|
||||
args_required.remove(contents)
|
||||
elif contents in arg_spec:
|
||||
raise CommandArgError(f"duplicate argument for {command}: {contents}")
|
||||
else:
|
||||
prefix_matches = []
|
||||
for switch in switches:
|
||||
if switch.startswith(contents):
|
||||
prefix_matches.append(switch)
|
||||
mapped.add(contents)
|
||||
continue
|
||||
|
||||
if len(prefix_matches) == 1:
|
||||
prefix_matches = []
|
||||
for switch in switches:
|
||||
if switch.startswith(contents):
|
||||
prefix_matches.append(switch)
|
||||
|
||||
if len(prefix_matches) == 1:
|
||||
raise CommandArgError(
|
||||
f"shortened argument for {command_name}: expand {contents} to"
|
||||
f" {prefix_matches[0]}"
|
||||
)
|
||||
|
||||
if len(prefix_matches) > 1:
|
||||
raise CommandArgError(
|
||||
f"ambiguous argument for {command_name}: {contents} could be any of"
|
||||
f" {', '.join(prefix_matches)}"
|
||||
)
|
||||
|
||||
raise CommandArgError(f"unrecognized argument for {command_name}: {contents}")
|
||||
|
||||
return mapped, positional_args
|
||||
|
||||
|
||||
def map_positionals(
|
||||
args: list[Node], spec: list[dict], command_name: str
|
||||
) -> list[list[int]]:
|
||||
"""Maps a list of nodes representing positional command arguments to the specific
|
||||
positional arguments of a command. spec represents the "positionals" entry of the
|
||||
spec for the given command.
|
||||
|
||||
The return value is a list whose entries correspond one-to-one to the entries in
|
||||
`args`. Each item in the return value is a list of indices into `spec`, indicating
|
||||
which argument(s) in the spec the corresponding argument maps to.
|
||||
|
||||
A given index into `spec` may appear multiple times in the list (e.g. if it's a
|
||||
variadic argument), and a list may contain more than one index for the mapping of an
|
||||
arg expansion.
|
||||
|
||||
If the arguments do not map correctly to the spec, this function raises
|
||||
CommandArgError.
|
||||
|
||||
Given a set of args and a spec, there may be multiple possible mappings. This
|
||||
function will return some mapping if one exists.
|
||||
|
||||
The `command_name` argument is used to generate descriptive error messages.
|
||||
"""
|
||||
|
||||
if len(args) == len(spec):
|
||||
# Self explanatory: a 1:1 match in argument count should be a legal mapping.
|
||||
return [[i] for i in range(len(args))]
|
||||
|
||||
mapping: list[list[int]] = []
|
||||
i = 0
|
||||
if len(args) > len(spec):
|
||||
# If there are more arguments than specified positionals, we map every argument
|
||||
# greedily and assign the extra # of arguments to the first variadic we find.
|
||||
extra = len(args) - len(spec)
|
||||
for arg in args:
|
||||
if i >= len(spec):
|
||||
# We never found a variadic to save us, raise an error.
|
||||
raise CommandArgError(
|
||||
f"shortened argument for {command}: expand {contents} to"
|
||||
f" {prefix_matches[0]}"
|
||||
f"too many arguments for {command_name}: got {len(args)}, expected"
|
||||
f" no more than {len(spec)}"
|
||||
)
|
||||
|
||||
if len(prefix_matches) > 1:
|
||||
raise CommandArgError(
|
||||
f"ambiguous argument for {command}: {contents} could be any of"
|
||||
f" {', '.join(prefix_matches)}"
|
||||
)
|
||||
mapping.append([i])
|
||||
if spec[i]["value"]["type"] == "variadic" and extra > 0:
|
||||
extra -= 1
|
||||
else:
|
||||
i += 1
|
||||
|
||||
raise CommandArgError(f"unrecognized argument for {command}: {contents}")
|
||||
return mapping
|
||||
|
||||
if len(args_required) > 1:
|
||||
raise CommandArgError(
|
||||
f"missing required arguments for {command}: {', '.join(args_required)}"
|
||||
)
|
||||
elif len(args_required) == 1:
|
||||
raise CommandArgError(
|
||||
f"missing required argument for {command}: {args_required.pop()}"
|
||||
)
|
||||
required = []
|
||||
for argspec in spec:
|
||||
if argspec["required"]:
|
||||
required.append(argspec["name"])
|
||||
num_required = len(required)
|
||||
|
||||
min_positionals = 0
|
||||
max_positionals: Optional[int] = 0
|
||||
for positional in arg_spec["positionals"]:
|
||||
if positional["value"]["type"] == "variadic":
|
||||
max_positionals = None
|
||||
if len(args) < num_required:
|
||||
# If there are fewer arguments than required positionals, we map only required
|
||||
# arguments and expand the first arg expansion we find to account for what's
|
||||
# missing.
|
||||
missing = num_required - len(args)
|
||||
for arg in args:
|
||||
while not spec[i]["required"]:
|
||||
i += 1
|
||||
|
||||
if positional["required"]:
|
||||
min_positionals += 1
|
||||
if max_positionals is not None:
|
||||
max_positionals += 1
|
||||
mapping.append([i])
|
||||
i += 1
|
||||
|
||||
check = check_count(
|
||||
command,
|
||||
min=min_positionals,
|
||||
max=max_positionals,
|
||||
args_name="positional args",
|
||||
)
|
||||
check(positional_args, None)
|
||||
if isinstance(arg, ArgExpansion):
|
||||
# Map missing arguments.
|
||||
while missing > 0:
|
||||
if spec[i]["required"]:
|
||||
mapping[-1] += [i]
|
||||
missing -= 1
|
||||
i += 1
|
||||
|
||||
return None
|
||||
if missing > 0:
|
||||
missing_names = ", ".join(required[-missing:])
|
||||
raise CommandArgError(
|
||||
f"missing required argument{'s' if missing > 1 else ''} for"
|
||||
f" {command_name}: {missing_names}"
|
||||
)
|
||||
|
||||
return mapping
|
||||
|
||||
optionals = len(args) - num_required
|
||||
for arg in args:
|
||||
# If our argument count falls somewhere in between the required and total
|
||||
# specified numbers of positionals, we map all required arguments and map as
|
||||
# many optionals as needed (as we find them).
|
||||
if not spec[i]["required"] and optionals > 0:
|
||||
mapping.append([i])
|
||||
i += 1
|
||||
optionals -= 1
|
||||
continue
|
||||
|
||||
while not spec[i]["required"]:
|
||||
i += 1
|
||||
|
||||
mapping.append([i])
|
||||
i += 1
|
||||
|
||||
return mapping
|
||||
|
||||
Reference in New Issue
Block a user