parser.py
744 lines
| 25.5 KiB
| text/x-python
|
PythonLexer
/ mercurial / parser.py
Matt Mackall
|
r11274 | # parser.py - simple top-down operator precedence parser for mercurial | ||
# | ||||
# Copyright 2010 Matt Mackall <mpm@selenic.com> | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
Julian Cowley
|
r11449 | # see http://effbot.org/zone/simple-top-down-parsing.htm and | ||
Matt Mackall
|
r11274 | # http://eli.thegreenplace.net/2010/01/02/top-down-operator-precedence-parsing/ | ||
# for background | ||||
# takes a tokenizer and elements | ||||
Yuya Nishihara
|
r25655 | # tokenizer is an iterator that returns (type, value, pos) tuples | ||
Yuya Nishihara
|
r25815 | # elements is a mapping of types to binding strength, primary, prefix, infix | ||
# and suffix actions | ||||
Matt Mackall
|
r11274 | # an action is a tree node name, a tree label, and an optional match | ||
timeless@mozdev.org
|
r17500 | # __call__(program) parses program into a labeled tree | ||
Matt Mackall
|
r11274 | |||
Yuya Nishihara
|
r34139 | from __future__ import absolute_import, print_function | ||
Gregory Szorc
|
r25963 | |||
from .i18n import _ | ||||
Yuya Nishihara
|
r31484 | from . import ( | ||
error, | ||||
Yuya Nishihara
|
r36565 | pycompat, | ||
Yuya Nishihara
|
r31484 | util, | ||
) | ||||
Augie Fackler
|
r43346 | from .utils import stringutil | ||
Matt Mackall
|
r11289 | |||
Matt Mackall
|
r11274 | class parser(object): | ||
Yuya Nishihara
|
r25654 | def __init__(self, elements, methods=None): | ||
Matt Mackall
|
r11274 | self._elements = elements | ||
self._methods = methods | ||||
Matt Mackall
|
r13176 | self.current = None | ||
Augie Fackler
|
r43346 | |||
Matt Mackall
|
r11274 | def _advance(self): | ||
Matt Harbison
|
r44226 | """advance the tokenizer""" | ||
Matt Mackall
|
r11274 | t = self.current | ||
Pierre-Yves David
|
r25171 | self.current = next(self._iter, None) | ||
Matt Mackall
|
r11274 | return t | ||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r25804 | def _hasnewterm(self): | ||
Matt Harbison
|
r44226 | """True if next token may start new term""" | ||
Yuya Nishihara
|
r25815 | return any(self._elements[self.current[0]][1:3]) | ||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r25802 | def _match(self, m): | ||
Matt Harbison
|
r44226 | """make sure the tokenizer matches an end condition""" | ||
Matt Mackall
|
r11274 | if self.current[0] != m: | ||
Augie Fackler
|
r43346 | raise error.ParseError( | ||
Augie Fackler
|
r43347 | _(b"unexpected token: %s") % self.current[0], self.current[2] | ||
Augie Fackler
|
r43346 | ) | ||
Matt Mackall
|
r11274 | self._advance() | ||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r25803 | def _parseoperand(self, bind, m=None): | ||
Matt Harbison
|
r44226 | """gather right-hand-side operand until an end condition or binding | ||
met""" | ||||
Yuya Nishihara
|
r25803 | if m and self.current[0] == m: | ||
expr = None | ||||
else: | ||||
expr = self._parse(bind) | ||||
if m: | ||||
self._match(m) | ||||
return expr | ||||
Augie Fackler
|
r43346 | |||
Matt Mackall
|
r11274 | def _parse(self, bind=0): | ||
Matt Mackall
|
r11289 | token, value, pos = self._advance() | ||
Yuya Nishihara
|
r25816 | # handle prefix rules on current token, take as primary if unambiguous | ||
Yuya Nishihara
|
r25815 | primary, prefix = self._elements[token][1:3] | ||
Yuya Nishihara
|
r25816 | if primary and not (prefix and self._hasnewterm()): | ||
Yuya Nishihara
|
r25815 | expr = (primary, value) | ||
elif prefix: | ||||
expr = (prefix[0], self._parseoperand(*prefix[1:])) | ||||
else: | ||||
Augie Fackler
|
r43347 | raise error.ParseError(_(b"not a prefix: %s") % token, pos) | ||
Matt Mackall
|
r11274 | # gather tokens until we meet a lower binding strength | ||
while bind < self._elements[self.current[0]][0]: | ||||
Matt Mackall
|
r11289 | token, value, pos = self._advance() | ||
Yuya Nishihara
|
r25817 | # handle infix rules, take as suffix if unambiguous | ||
Yuya Nishihara
|
r25815 | infix, suffix = self._elements[token][3:] | ||
Yuya Nishihara
|
r25818 | if suffix and not (infix and self._hasnewterm()): | ||
Yuya Nishihara
|
r29767 | expr = (suffix, expr) | ||
Yuya Nishihara
|
r25817 | elif infix: | ||
expr = (infix[0], expr, self._parseoperand(*infix[1:])) | ||||
Matt Mackall
|
r11274 | else: | ||
Augie Fackler
|
r43347 | raise error.ParseError(_(b"not an infix: %s") % token, pos) | ||
Matt Mackall
|
r11274 | return expr | ||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r25654 | def parse(self, tokeniter): | ||
Matt Harbison
|
r44226 | """generate a parse tree from tokens""" | ||
Yuya Nishihara
|
r25654 | self._iter = tokeniter | ||
Matt Mackall
|
r13176 | self._advance() | ||
Bernhard Leiner
|
r13665 | res = self._parse() | ||
token, value, pos = self.current | ||||
return res, pos | ||||
Augie Fackler
|
r43346 | |||
Matt Mackall
|
r11274 | def eval(self, tree): | ||
Matt Harbison
|
r44226 | """recursively evaluate a parse tree using node methods""" | ||
Matt Mackall
|
r11274 | if not isinstance(tree, tuple): | ||
return tree | ||||
return self._methods[tree[0]](*[self.eval(t) for t in tree[1:]]) | ||||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r25654 | def __call__(self, tokeniter): | ||
Matt Harbison
|
r44226 | """parse tokens into a parse tree and evaluate if methods given""" | ||
Yuya Nishihara
|
r25654 | t = self.parse(tokeniter) | ||
Matt Mackall
|
r11274 | if self._methods: | ||
return self.eval(t) | ||||
return t | ||||
Yuya Nishihara
|
r25253 | |||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r30753 | def splitargspec(spec): | ||
Yuya Nishihara
|
r31921 | """Parse spec of function arguments into (poskeys, varkey, keys, optkey) | ||
Yuya Nishihara
|
r30753 | |||
Yuya Nishihara
|
r34133 | >>> splitargspec(b'') | ||
Yuya Nishihara
|
r31921 | ([], None, [], None) | ||
Yuya Nishihara
|
r34133 | >>> splitargspec(b'foo bar') | ||
Yuya Nishihara
|
r31921 | ([], None, ['foo', 'bar'], None) | ||
Yuya Nishihara
|
r34133 | >>> splitargspec(b'foo *bar baz **qux') | ||
Yuya Nishihara
|
r31921 | (['foo'], 'bar', ['baz'], 'qux') | ||
Yuya Nishihara
|
r34133 | >>> splitargspec(b'*foo') | ||
Yuya Nishihara
|
r31921 | ([], 'foo', [], None) | ||
Yuya Nishihara
|
r34133 | >>> splitargspec(b'**foo') | ||
Yuya Nishihara
|
r31921 | ([], None, [], 'foo') | ||
Yuya Nishihara
|
r30753 | """ | ||
Yuya Nishihara
|
r31921 | optkey = None | ||
Augie Fackler
|
r43347 | pre, sep, post = spec.partition(b'**') | ||
Yuya Nishihara
|
r31921 | if sep: | ||
posts = post.split() | ||||
if not posts: | ||||
Augie Fackler
|
r43347 | raise error.ProgrammingError(b'no **optkey name provided') | ||
Yuya Nishihara
|
r31921 | if len(posts) > 1: | ||
Augie Fackler
|
r43347 | raise error.ProgrammingError(b'excessive **optkey names provided') | ||
Yuya Nishihara
|
r31921 | optkey = posts[0] | ||
Augie Fackler
|
r43347 | pre, sep, post = pre.partition(b'*') | ||
Yuya Nishihara
|
r30753 | pres = pre.split() | ||
posts = post.split() | ||||
if sep: | ||||
if not posts: | ||||
Augie Fackler
|
r43347 | raise error.ProgrammingError(b'no *varkey name provided') | ||
Yuya Nishihara
|
r31921 | return pres, posts[0], posts[1:], optkey | ||
return [], None, pres, optkey | ||||
Yuya Nishihara
|
r30753 | |||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r30753 | def buildargsdict(trees, funcname, argspec, keyvaluenode, keynode): | ||
Yuya Nishihara
|
r25705 | """Build dict from list containing positional and keyword arguments | ||
Yuya Nishihara
|
r31921 | Arguments are specified by a tuple of ``(poskeys, varkey, keys, optkey)`` | ||
where | ||||
Yuya Nishihara
|
r30753 | |||
- ``poskeys``: list of names of positional arguments | ||||
- ``varkey``: optional argument name that takes up remainder | ||||
- ``keys``: list of names that can be either positional or keyword arguments | ||||
Yuya Nishihara
|
r31921 | - ``optkey``: optional argument name that takes up excess keyword arguments | ||
Yuya Nishihara
|
r30753 | |||
If ``varkey`` specified, all ``keys`` must be given as keyword arguments. | ||||
Invalid keywords, too few positional arguments, or too many positional | ||||
arguments are rejected, but missing keyword arguments are just omitted. | ||||
Yuya Nishihara
|
r25705 | """ | ||
Yuya Nishihara
|
r31921 | poskeys, varkey, keys, optkey = argspec | ||
Augie Fackler
|
r43346 | kwstart = next( | ||
(i for i, x in enumerate(trees) if x and x[0] == keyvaluenode), | ||||
len(trees), | ||||
) | ||||
Yuya Nishihara
|
r30753 | if kwstart < len(poskeys): | ||
Augie Fackler
|
r43346 | raise error.ParseError( | ||
Martin von Zweigbergk
|
r43387 | _(b"%(func)s takes at least %(nargs)d positional arguments") | ||
Augie Fackler
|
r43347 | % {b'func': funcname, b'nargs': len(poskeys)} | ||
Augie Fackler
|
r43346 | ) | ||
Yuya Nishihara
|
r31920 | if not varkey and kwstart > len(poskeys) + len(keys): | ||
Augie Fackler
|
r43346 | raise error.ParseError( | ||
Martin von Zweigbergk
|
r43387 | _(b"%(func)s takes at most %(nargs)d positional arguments") | ||
Augie Fackler
|
r43347 | % {b'func': funcname, b'nargs': len(poskeys) + len(keys)} | ||
Augie Fackler
|
r43346 | ) | ||
Yuya Nishihara
|
r31922 | args = util.sortdict() | ||
Yuya Nishihara
|
r25705 | # consume positional arguments | ||
Yuya Nishihara
|
r30753 | for k, x in zip(poskeys, trees[:kwstart]): | ||
Yuya Nishihara
|
r25705 | args[k] = x | ||
Yuya Nishihara
|
r30753 | if varkey: | ||
Augie Fackler
|
r43346 | args[varkey] = trees[len(args) : kwstart] | ||
Yuya Nishihara
|
r30753 | else: | ||
Augie Fackler
|
r43346 | for k, x in zip(keys, trees[len(args) : kwstart]): | ||
Yuya Nishihara
|
r30753 | args[k] = x | ||
Yuya Nishihara
|
r25705 | # remainder should be keyword arguments | ||
Yuya Nishihara
|
r31921 | if optkey: | ||
Yuya Nishihara
|
r31922 | args[optkey] = util.sortdict() | ||
Yuya Nishihara
|
r30752 | for x in trees[kwstart:]: | ||
Yuya Nishihara
|
r42420 | if not x or x[0] != keyvaluenode or x[1][0] != keynode: | ||
Augie Fackler
|
r43346 | raise error.ParseError( | ||
Augie Fackler
|
r43347 | _(b"%(func)s got an invalid argument") % {b'func': funcname} | ||
Augie Fackler
|
r43346 | ) | ||
Yuya Nishihara
|
r25705 | k = x[1][1] | ||
Yuya Nishihara
|
r31921 | if k in keys: | ||
d = args | ||||
elif not optkey: | ||||
Augie Fackler
|
r43346 | raise error.ParseError( | ||
Martin von Zweigbergk
|
r43387 | _(b"%(func)s got an unexpected keyword argument '%(key)s'") | ||
Augie Fackler
|
r43347 | % {b'func': funcname, b'key': k} | ||
Augie Fackler
|
r43346 | ) | ||
Yuya Nishihara
|
r31921 | else: | ||
d = args[optkey] | ||||
if k in d: | ||||
Augie Fackler
|
r43346 | raise error.ParseError( | ||
_( | ||||
Augie Fackler
|
r43347 | b"%(func)s got multiple values for keyword " | ||
b"argument '%(key)s'" | ||||
Augie Fackler
|
r43346 | ) | ||
Augie Fackler
|
r43347 | % {b'func': funcname, b'key': k} | ||
Augie Fackler
|
r43346 | ) | ||
Yuya Nishihara
|
r31921 | d[k] = x[2] | ||
Yuya Nishihara
|
r25705 | return args | ||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r26231 | def unescapestr(s): | ||
try: | ||||
Yuya Nishihara
|
r37102 | return stringutil.unescapestr(s) | ||
Yuya Nishihara
|
r26231 | except ValueError as e: | ||
# mangle Python's exception into our format | ||||
Yuya Nishihara
|
r36565 | raise error.ParseError(pycompat.bytestr(e).lower()) | ||
Yuya Nishihara
|
r26231 | |||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r25254 | def _prettyformat(tree, leafnodes, level, lines): | ||
Yuya Nishihara
|
r34075 | if not isinstance(tree, tuple): | ||
Augie Fackler
|
r39085 | lines.append((level, stringutil.pprint(tree))) | ||
Yuya Nishihara
|
r34075 | elif tree[0] in leafnodes: | ||
Augie Fackler
|
r39085 | rs = map(stringutil.pprint, tree[1:]) | ||
Augie Fackler
|
r43347 | lines.append((level, b'(%s %s)' % (tree[0], b' '.join(rs)))) | ||
Yuya Nishihara
|
r25254 | else: | ||
Augie Fackler
|
r43347 | lines.append((level, b'(%s' % tree[0])) | ||
Yuya Nishihara
|
r25254 | for s in tree[1:]: | ||
_prettyformat(s, leafnodes, level + 1, lines) | ||||
Augie Fackler
|
r43347 | lines[-1:] = [(lines[-1][0], lines[-1][1] + b')')] | ||
Yuya Nishihara
|
r25254 | |||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r25253 | def prettyformat(tree, leafnodes): | ||
lines = [] | ||||
Yuya Nishihara
|
r25254 | _prettyformat(tree, leafnodes, 0, lines) | ||
Augie Fackler
|
r43347 | output = b'\n'.join((b' ' * l + s) for l, s in lines) | ||
Yuya Nishihara
|
r25253 | return output | ||
Yuya Nishihara
|
r25306 | |||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r25306 | def simplifyinfixops(tree, targetnodes): | ||
"""Flatten chained infix operations to reduce usage of Python stack | ||||
Yuya Nishihara
|
r34139 | >>> from . import pycompat | ||
Yuya Nishihara
|
r25306 | >>> def f(tree): | ||
Yuya Nishihara
|
r34139 | ... s = prettyformat(simplifyinfixops(tree, (b'or',)), (b'symbol',)) | ||
... print(pycompat.sysstr(s)) | ||||
Yuya Nishihara
|
r34133 | >>> f((b'or', | ||
... (b'or', | ||||
... (b'symbol', b'1'), | ||||
... (b'symbol', b'2')), | ||||
... (b'symbol', b'3'))) | ||||
Yuya Nishihara
|
r25306 | (or | ||
Yuya Nishihara
|
r34075 | (symbol '1') | ||
(symbol '2') | ||||
(symbol '3')) | ||||
Yuya Nishihara
|
r34133 | >>> f((b'func', | ||
... (b'symbol', b'p1'), | ||||
... (b'or', | ||||
... (b'or', | ||||
... (b'func', | ||||
... (b'symbol', b'sort'), | ||||
... (b'list', | ||||
... (b'or', | ||||
... (b'or', | ||||
... (b'symbol', b'1'), | ||||
... (b'symbol', b'2')), | ||||
... (b'symbol', b'3')), | ||||
... (b'negate', | ||||
... (b'symbol', b'rev')))), | ||||
... (b'and', | ||||
... (b'symbol', b'4'), | ||||
... (b'group', | ||||
... (b'or', | ||||
... (b'or', | ||||
... (b'symbol', b'5'), | ||||
... (b'symbol', b'6')), | ||||
... (b'symbol', b'7'))))), | ||||
... (b'symbol', b'8')))) | ||||
Yuya Nishihara
|
r25306 | (func | ||
Yuya Nishihara
|
r34075 | (symbol 'p1') | ||
Yuya Nishihara
|
r25306 | (or | ||
(func | ||||
Yuya Nishihara
|
r34075 | (symbol 'sort') | ||
Yuya Nishihara
|
r25306 | (list | ||
(or | ||||
Yuya Nishihara
|
r34075 | (symbol '1') | ||
(symbol '2') | ||||
(symbol '3')) | ||||
Yuya Nishihara
|
r25306 | (negate | ||
Yuya Nishihara
|
r34075 | (symbol 'rev')))) | ||
Yuya Nishihara
|
r25306 | (and | ||
Yuya Nishihara
|
r34075 | (symbol '4') | ||
Yuya Nishihara
|
r25306 | (group | ||
(or | ||||
Yuya Nishihara
|
r34075 | (symbol '5') | ||
(symbol '6') | ||||
(symbol '7')))) | ||||
(symbol '8'))) | ||||
Yuya Nishihara
|
r25306 | """ | ||
if not isinstance(tree, tuple): | ||||
return tree | ||||
op = tree[0] | ||||
if op not in targetnodes: | ||||
return (op,) + tuple(simplifyinfixops(x, targetnodes) for x in tree[1:]) | ||||
# walk down left nodes taking each right node. no recursion to left nodes | ||||
# because infix operators are left-associative, i.e. left tree is deep. | ||||
# e.g. '1 + 2 + 3' -> (+ (+ 1 2) 3) -> (+ 1 2 3) | ||||
simplified = [] | ||||
x = tree | ||||
while x[0] == op: | ||||
l, r = x[1:] | ||||
simplified.append(simplifyinfixops(r, targetnodes)) | ||||
x = l | ||||
simplified.append(simplifyinfixops(x, targetnodes)) | ||||
simplified.append(op) | ||||
return tuple(reversed(simplified)) | ||||
Yuya Nishihara
|
r28720 | |||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r34045 | def _buildtree(template, placeholder, replstack): | ||
if template == placeholder: | ||||
return replstack.pop() | ||||
if not isinstance(template, tuple): | ||||
return template | ||||
return tuple(_buildtree(x, placeholder, replstack) for x in template) | ||||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r34045 | def buildtree(template, placeholder, *repls): | ||
"""Create new tree by substituting placeholders by replacements | ||||
Yuya Nishihara
|
r34133 | >>> _ = (b'symbol', b'_') | ||
Yuya Nishihara
|
r34045 | >>> def f(template, *repls): | ||
... return buildtree(template, _, *repls) | ||||
Yuya Nishihara
|
r34133 | >>> f((b'func', (b'symbol', b'only'), (b'list', _, _)), | ||
Yuya Nishihara
|
r34045 | ... ('symbol', '1'), ('symbol', '2')) | ||
('func', ('symbol', 'only'), ('list', ('symbol', '1'), ('symbol', '2'))) | ||||
Yuya Nishihara
|
r34133 | >>> f((b'and', _, (b'not', _)), (b'symbol', b'1'), (b'symbol', b'2')) | ||
Yuya Nishihara
|
r34045 | ('and', ('symbol', '1'), ('not', ('symbol', '2'))) | ||
""" | ||||
if not isinstance(placeholder, tuple): | ||||
Augie Fackler
|
r43347 | raise error.ProgrammingError(b'placeholder must be a node tuple') | ||
Yuya Nishihara
|
r34045 | replstack = list(reversed(repls)) | ||
r = _buildtree(template, placeholder, replstack) | ||||
if replstack: | ||||
Augie Fackler
|
r43347 | raise error.ProgrammingError(b'too many replacements') | ||
Yuya Nishihara
|
r34045 | return r | ||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r34047 | def _matchtree(pattern, tree, placeholder, incompletenodes, matches): | ||
if pattern == tree: | ||||
return True | ||||
if not isinstance(pattern, tuple) or not isinstance(tree, tuple): | ||||
return False | ||||
if pattern == placeholder and tree[0] not in incompletenodes: | ||||
matches.append(tree) | ||||
return True | ||||
if len(pattern) != len(tree): | ||||
return False | ||||
Augie Fackler
|
r43346 | return all( | ||
_matchtree(p, x, placeholder, incompletenodes, matches) | ||||
for p, x in zip(pattern, tree) | ||||
) | ||||
Yuya Nishihara
|
r34047 | |||
def matchtree(pattern, tree, placeholder=None, incompletenodes=()): | ||||
"""If a tree matches the pattern, return a list of the tree and nodes | ||||
matched with the placeholder; Otherwise None | ||||
>>> def f(pattern, tree): | ||||
Yuya Nishihara
|
r34133 | ... m = matchtree(pattern, tree, _, {b'keyvalue', b'list'}) | ||
Yuya Nishihara
|
r34047 | ... if m: | ||
... return m[1:] | ||||
Yuya Nishihara
|
r34133 | >>> _ = (b'symbol', b'_') | ||
>>> f((b'func', (b'symbol', b'ancestors'), _), | ||||
... (b'func', (b'symbol', b'ancestors'), (b'symbol', b'1'))) | ||||
Yuya Nishihara
|
r34047 | [('symbol', '1')] | ||
Yuya Nishihara
|
r34133 | >>> f((b'func', (b'symbol', b'ancestors'), _), | ||
... (b'func', (b'symbol', b'ancestors'), None)) | ||||
>>> f((b'range', (b'dagrange', _, _), _), | ||||
... (b'range', | ||||
... (b'dagrange', (b'symbol', b'1'), (b'symbol', b'2')), | ||||
... (b'symbol', b'3'))) | ||||
Yuya Nishihara
|
r34047 | [('symbol', '1'), ('symbol', '2'), ('symbol', '3')] | ||
The placeholder does not match the specified incomplete nodes because | ||||
an incomplete node (e.g. argument list) cannot construct an expression. | ||||
Yuya Nishihara
|
r34133 | >>> f((b'func', (b'symbol', b'ancestors'), _), | ||
... (b'func', (b'symbol', b'ancestors'), | ||||
... (b'list', (b'symbol', b'1'), (b'symbol', b'2')))) | ||||
Yuya Nishihara
|
r34047 | |||
The placeholder may be omitted, but which shouldn't match a None node. | ||||
>>> _ = None | ||||
Yuya Nishihara
|
r34133 | >>> f((b'func', (b'symbol', b'ancestors'), None), | ||
... (b'func', (b'symbol', b'ancestors'), (b'symbol', b'0'))) | ||||
Yuya Nishihara
|
r34047 | """ | ||
if placeholder is not None and not isinstance(placeholder, tuple): | ||||
Augie Fackler
|
r43347 | raise error.ProgrammingError(b'placeholder must be a node tuple') | ||
Yuya Nishihara
|
r34047 | matches = [tree] | ||
if _matchtree(pattern, tree, placeholder, incompletenodes, matches): | ||||
return matches | ||||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r28720 | def parseerrordetail(inst): | ||
"""Compose error message from specified ParseError object | ||||
""" | ||||
Martin von Zweigbergk
|
r46361 | if inst.location is not None: | ||
return _(b'at %d: %s') % (inst.location, inst.message) | ||||
Yuya Nishihara
|
r28720 | else: | ||
Martin von Zweigbergk
|
r46361 | return inst.message | ||
Yuya Nishihara
|
r28870 | |||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r28892 | class alias(object): | ||
"""Parsed result of alias""" | ||||
Yuya Nishihara
|
r28908 | def __init__(self, name, args, err, replacement): | ||
Yuya Nishihara
|
r28892 | self.name = name | ||
self.args = args | ||||
self.error = err | ||||
self.replacement = replacement | ||||
# whether own `error` information is already shown or not. | ||||
Yuya Nishihara
|
r28898 | # this avoids showing same warning multiple times at each | ||
# `expandaliases`. | ||||
Yuya Nishihara
|
r28892 | self.warned = False | ||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r28870 | class basealiasrules(object): | ||
"""Parsing and expansion rule set of aliases | ||||
This is a helper for fileset/revset/template aliases. A concrete rule set | ||||
should be made by sub-classing this and implementing class/static methods. | ||||
Mads Kiilerich
|
r30332 | It supports alias expansion of symbol and function-call styles:: | ||
Yuya Nishihara
|
r28870 | |||
# decl = defn | ||||
h = heads(default) | ||||
b($1) = ancestors($1) - ancestors(default) | ||||
""" | ||||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r28870 | # typically a config section, which will be included in error messages | ||
_section = None | ||||
Yuya Nishihara
|
r28910 | # tag of symbol node | ||
Augie Fackler
|
r43347 | _symbolnode = b'symbol' | ||
Yuya Nishihara
|
r28870 | |||
def __new__(cls): | ||||
Augie Fackler
|
r43347 | raise TypeError(b"'%s' is not instantiatable" % cls.__name__) | ||
Yuya Nishihara
|
r28870 | |||
@staticmethod | ||||
Yuya Nishihara
|
r28875 | def _parse(spec): | ||
"""Parse an alias name, arguments and definition""" | ||||
Yuya Nishihara
|
r28870 | raise NotImplementedError | ||
@staticmethod | ||||
Yuya Nishihara
|
r28910 | def _trygetfunc(tree): | ||
"""Return (name, args) if tree is a function; otherwise None""" | ||||
Yuya Nishihara
|
r28870 | raise NotImplementedError | ||
Yuya Nishihara
|
r28871 | |||
@classmethod | ||||
def _builddecl(cls, decl): | ||||
Yuya Nishihara
|
r28908 | """Parse an alias declaration into ``(name, args, errorstr)`` | ||
Yuya Nishihara
|
r28871 | |||
This function analyzes the parsed tree. The parsing rule is provided | ||||
Yuya Nishihara
|
r28875 | by ``_parse()``. | ||
Yuya Nishihara
|
r28871 | |||
- ``name``: of declared alias (may be ``decl`` itself at error) | ||||
- ``args``: list of argument names (or None for symbol declaration) | ||||
- ``errorstr``: detail about detected error (or None) | ||||
Yuya Nishihara
|
r34133 | >>> sym = lambda x: (b'symbol', x) | ||
>>> symlist = lambda *xs: (b'list',) + tuple(sym(x) for x in xs) | ||||
>>> func = lambda n, a: (b'func', sym(n), a) | ||||
Yuya Nishihara
|
r28871 | >>> parsemap = { | ||
Yuya Nishihara
|
r34133 | ... b'foo': sym(b'foo'), | ||
... b'$foo': sym(b'$foo'), | ||||
... b'foo::bar': (b'dagrange', sym(b'foo'), sym(b'bar')), | ||||
... b'foo()': func(b'foo', None), | ||||
... b'$foo()': func(b'$foo', None), | ||||
... b'foo($1, $2)': func(b'foo', symlist(b'$1', b'$2')), | ||||
... b'foo(bar_bar, baz.baz)': | ||||
... func(b'foo', symlist(b'bar_bar', b'baz.baz')), | ||||
... b'foo(bar($1, $2))': | ||||
... func(b'foo', func(b'bar', symlist(b'$1', b'$2'))), | ||||
... b'foo($1, $2, nested($1, $2))': | ||||
... func(b'foo', (symlist(b'$1', b'$2') + | ||||
... (func(b'nested', symlist(b'$1', b'$2')),))), | ||||
... b'foo("bar")': func(b'foo', (b'string', b'bar')), | ||||
... b'foo($1, $2': error.ParseError(b'unexpected token: end', 10), | ||||
... b'foo("bar': error.ParseError(b'unterminated string', 5), | ||||
... b'foo($1, $2, $1)': func(b'foo', symlist(b'$1', b'$2', b'$1')), | ||||
Yuya Nishihara
|
r28871 | ... } | ||
>>> def parse(expr): | ||||
... x = parsemap[expr] | ||||
... if isinstance(x, Exception): | ||||
... raise x | ||||
... return x | ||||
Yuya Nishihara
|
r28910 | >>> def trygetfunc(tree): | ||
Yuya Nishihara
|
r34133 | ... if not tree or tree[0] != b'func' or tree[1][0] != b'symbol': | ||
Yuya Nishihara
|
r28910 | ... return None | ||
... if not tree[2]: | ||||
... return tree[1][1], [] | ||||
Yuya Nishihara
|
r34133 | ... if tree[2][0] == b'list': | ||
Yuya Nishihara
|
r28910 | ... return tree[1][1], list(tree[2][1:]) | ||
... return tree[1][1], [tree[2]] | ||||
Yuya Nishihara
|
r28871 | >>> class aliasrules(basealiasrules): | ||
Yuya Nishihara
|
r28875 | ... _parse = staticmethod(parse) | ||
Yuya Nishihara
|
r28910 | ... _trygetfunc = staticmethod(trygetfunc) | ||
Yuya Nishihara
|
r28871 | >>> builddecl = aliasrules._builddecl | ||
Yuya Nishihara
|
r34133 | >>> builddecl(b'foo') | ||
Yuya Nishihara
|
r28908 | ('foo', None, None) | ||
Yuya Nishihara
|
r34133 | >>> builddecl(b'$foo') | ||
Yuya Nishihara
|
r29058 | ('$foo', None, "invalid symbol '$foo'") | ||
Yuya Nishihara
|
r34133 | >>> builddecl(b'foo::bar') | ||
Yuya Nishihara
|
r28908 | ('foo::bar', None, 'invalid format') | ||
Yuya Nishihara
|
r34133 | >>> builddecl(b'foo()') | ||
Yuya Nishihara
|
r28908 | ('foo', [], None) | ||
Yuya Nishihara
|
r34133 | >>> builddecl(b'$foo()') | ||
Yuya Nishihara
|
r29058 | ('$foo()', None, "invalid function '$foo'") | ||
Yuya Nishihara
|
r34133 | >>> builddecl(b'foo($1, $2)') | ||
Yuya Nishihara
|
r28908 | ('foo', ['$1', '$2'], None) | ||
Yuya Nishihara
|
r34133 | >>> builddecl(b'foo(bar_bar, baz.baz)') | ||
Yuya Nishihara
|
r28908 | ('foo', ['bar_bar', 'baz.baz'], None) | ||
Yuya Nishihara
|
r34133 | >>> builddecl(b'foo($1, $2, nested($1, $2))') | ||
Yuya Nishihara
|
r28908 | ('foo($1, $2, nested($1, $2))', None, 'invalid argument list') | ||
Yuya Nishihara
|
r34133 | >>> builddecl(b'foo(bar($1, $2))') | ||
Yuya Nishihara
|
r28908 | ('foo(bar($1, $2))', None, 'invalid argument list') | ||
Yuya Nishihara
|
r34133 | >>> builddecl(b'foo("bar")') | ||
Yuya Nishihara
|
r28908 | ('foo("bar")', None, 'invalid argument list') | ||
Yuya Nishihara
|
r34133 | >>> builddecl(b'foo($1, $2') | ||
Yuya Nishihara
|
r28908 | ('foo($1, $2', None, 'at 10: unexpected token: end') | ||
Yuya Nishihara
|
r34133 | >>> builddecl(b'foo("bar') | ||
Yuya Nishihara
|
r28908 | ('foo("bar', None, 'at 5: unterminated string') | ||
Yuya Nishihara
|
r34133 | >>> builddecl(b'foo($1, $2, $1)') | ||
Yuya Nishihara
|
r28908 | ('foo', None, 'argument names collide with each other') | ||
Yuya Nishihara
|
r28871 | """ | ||
try: | ||||
Yuya Nishihara
|
r28875 | tree = cls._parse(decl) | ||
Yuya Nishihara
|
r28871 | except error.ParseError as inst: | ||
Yuya Nishihara
|
r28908 | return (decl, None, parseerrordetail(inst)) | ||
Yuya Nishihara
|
r28871 | |||
if tree[0] == cls._symbolnode: | ||||
# "name = ...." style | ||||
name = tree[1] | ||||
Augie Fackler
|
r43347 | if name.startswith(b'$'): | ||
return (decl, None, _(b"invalid symbol '%s'") % name) | ||||
Yuya Nishihara
|
r28908 | return (name, None, None) | ||
Yuya Nishihara
|
r28871 | |||
Yuya Nishihara
|
r28910 | func = cls._trygetfunc(tree) | ||
if func: | ||||
Yuya Nishihara
|
r28871 | # "name(arg, ....) = ...." style | ||
Yuya Nishihara
|
r28910 | name, args = func | ||
Augie Fackler
|
r43347 | if name.startswith(b'$'): | ||
return (decl, None, _(b"invalid function '%s'") % name) | ||||
Yuya Nishihara
|
r28910 | if any(t[0] != cls._symbolnode for t in args): | ||
Augie Fackler
|
r43347 | return (decl, None, _(b"invalid argument list")) | ||
Yuya Nishihara
|
r28871 | if len(args) != len(set(args)): | ||
Augie Fackler
|
r43347 | return ( | ||
name, | ||||
None, | ||||
_(b"argument names collide with each other"), | ||||
) | ||||
Yuya Nishihara
|
r28910 | return (name, [t[1] for t in args], None) | ||
Yuya Nishihara
|
r28871 | |||
Augie Fackler
|
r43347 | return (decl, None, _(b"invalid format")) | ||
Yuya Nishihara
|
r28872 | |||
@classmethod | ||||
def _relabelargs(cls, tree, args): | ||||
"""Mark alias arguments as ``_aliasarg``""" | ||||
if not isinstance(tree, tuple): | ||||
return tree | ||||
op = tree[0] | ||||
if op != cls._symbolnode: | ||||
return (op,) + tuple(cls._relabelargs(x, args) for x in tree[1:]) | ||||
assert len(tree) == 2 | ||||
sym = tree[1] | ||||
if sym in args: | ||||
Augie Fackler
|
r43347 | op = b'_aliasarg' | ||
elif sym.startswith(b'$'): | ||||
raise error.ParseError(_(b"invalid symbol '%s'") % sym) | ||||
Yuya Nishihara
|
r28872 | return (op, sym) | ||
Yuya Nishihara
|
r28873 | |||
@classmethod | ||||
def _builddefn(cls, defn, args): | ||||
"""Parse an alias definition into a tree and marks substitutions | ||||
This function marks alias argument references as ``_aliasarg``. The | ||||
Yuya Nishihara
|
r28875 | parsing rule is provided by ``_parse()``. | ||
Yuya Nishihara
|
r28873 | |||
``args`` is a list of alias argument names, or None if the alias | ||||
is declared as a symbol. | ||||
Yuya Nishihara
|
r34139 | >>> from . import pycompat | ||
Yuya Nishihara
|
r28873 | >>> parsemap = { | ||
Yuya Nishihara
|
r34133 | ... b'$1 or foo': (b'or', (b'symbol', b'$1'), (b'symbol', b'foo')), | ||
... b'$1 or $bar': | ||||
... (b'or', (b'symbol', b'$1'), (b'symbol', b'$bar')), | ||||
... b'$10 or baz': | ||||
... (b'or', (b'symbol', b'$10'), (b'symbol', b'baz')), | ||||
... b'"$1" or "foo"': | ||||
... (b'or', (b'string', b'$1'), (b'string', b'foo')), | ||||
Yuya Nishihara
|
r28873 | ... } | ||
>>> class aliasrules(basealiasrules): | ||||
Yuya Nishihara
|
r28875 | ... _parse = staticmethod(parsemap.__getitem__) | ||
Yuya Nishihara
|
r28910 | ... _trygetfunc = staticmethod(lambda x: None) | ||
Yuya Nishihara
|
r28873 | >>> builddefn = aliasrules._builddefn | ||
>>> def pprint(tree): | ||||
Yuya Nishihara
|
r34139 | ... s = prettyformat(tree, (b'_aliasarg', b'string', b'symbol')) | ||
... print(pycompat.sysstr(s)) | ||||
Yuya Nishihara
|
r34133 | >>> args = [b'$1', b'$2', b'foo'] | ||
>>> pprint(builddefn(b'$1 or foo', args)) | ||||
Yuya Nishihara
|
r28873 | (or | ||
Yuya Nishihara
|
r34075 | (_aliasarg '$1') | ||
(_aliasarg 'foo')) | ||||
Yuya Nishihara
|
r28873 | >>> try: | ||
Yuya Nishihara
|
r34133 | ... builddefn(b'$1 or $bar', args) | ||
Yuya Nishihara
|
r28873 | ... except error.ParseError as inst: | ||
Yuya Nishihara
|
r34139 | ... print(pycompat.sysstr(parseerrordetail(inst))) | ||
Yuya Nishihara
|
r29058 | invalid symbol '$bar' | ||
Yuya Nishihara
|
r34133 | >>> args = [b'$1', b'$10', b'foo'] | ||
>>> pprint(builddefn(b'$10 or baz', args)) | ||||
Yuya Nishihara
|
r28873 | (or | ||
Yuya Nishihara
|
r34075 | (_aliasarg '$10') | ||
(symbol 'baz')) | ||||
Yuya Nishihara
|
r34133 | >>> pprint(builddefn(b'"$1" or "foo"', args)) | ||
Yuya Nishihara
|
r28873 | (or | ||
Yuya Nishihara
|
r34075 | (string '$1') | ||
(string 'foo')) | ||||
Yuya Nishihara
|
r28873 | """ | ||
Yuya Nishihara
|
r28875 | tree = cls._parse(defn) | ||
Yuya Nishihara
|
r28873 | if args: | ||
args = set(args) | ||||
else: | ||||
args = set() | ||||
return cls._relabelargs(tree, args) | ||||
Yuya Nishihara
|
r28892 | |||
@classmethod | ||||
def build(cls, decl, defn): | ||||
"""Parse an alias declaration and definition into an alias object""" | ||||
repl = efmt = None | ||||
Yuya Nishihara
|
r28908 | name, args, err = cls._builddecl(decl) | ||
Yuya Nishihara
|
r28892 | if err: | ||
Augie Fackler
|
r43347 | efmt = _(b'bad declaration of %(section)s "%(name)s": %(error)s') | ||
Yuya Nishihara
|
r28892 | else: | ||
try: | ||||
repl = cls._builddefn(defn, args) | ||||
except error.ParseError as inst: | ||||
err = parseerrordetail(inst) | ||||
Augie Fackler
|
r43347 | efmt = _(b'bad definition of %(section)s "%(name)s": %(error)s') | ||
Yuya Nishihara
|
r28892 | if err: | ||
Augie Fackler
|
r43347 | err = efmt % { | ||
b'section': cls._section, | ||||
b'name': name, | ||||
b'error': err, | ||||
} | ||||
Yuya Nishihara
|
r28908 | return alias(name, args, err, repl) | ||
Yuya Nishihara
|
r28893 | |||
@classmethod | ||||
def buildmap(cls, items): | ||||
"""Parse a list of alias (name, replacement) pairs into a dict of | ||||
alias objects""" | ||||
aliases = {} | ||||
for decl, defn in items: | ||||
a = cls.build(decl, defn) | ||||
aliases[a.name] = a | ||||
return aliases | ||||
Yuya Nishihara
|
r28895 | |||
@classmethod | ||||
def _getalias(cls, aliases, tree): | ||||
Yuya Nishihara
|
r28909 | """If tree looks like an unexpanded alias, return (alias, pattern-args) | ||
pair. Return None otherwise. | ||||
Yuya Nishihara
|
r28895 | """ | ||
if not isinstance(tree, tuple): | ||||
return None | ||||
if tree[0] == cls._symbolnode: | ||||
name = tree[1] | ||||
a = aliases.get(name) | ||||
Yuya Nishihara
|
r28908 | if a and a.args is None: | ||
Yuya Nishihara
|
r28909 | return a, None | ||
Yuya Nishihara
|
r28910 | func = cls._trygetfunc(tree) | ||
if func: | ||||
name, args = func | ||||
Yuya Nishihara
|
r28895 | a = aliases.get(name) | ||
Yuya Nishihara
|
r28908 | if a and a.args is not None: | ||
Yuya Nishihara
|
r28910 | return a, args | ||
Yuya Nishihara
|
r28895 | return None | ||
@classmethod | ||||
def _expandargs(cls, tree, args): | ||||
"""Replace _aliasarg instances with the substitution value of the | ||||
same name in args, recursively. | ||||
""" | ||||
if not isinstance(tree, tuple): | ||||
return tree | ||||
Augie Fackler
|
r43347 | if tree[0] == b'_aliasarg': | ||
Yuya Nishihara
|
r28895 | sym = tree[1] | ||
return args[sym] | ||||
return tuple(cls._expandargs(t, args) for t in tree) | ||||
@classmethod | ||||
def _expand(cls, aliases, tree, expanding, cache): | ||||
if not isinstance(tree, tuple): | ||||
return tree | ||||
Yuya Nishihara
|
r28909 | r = cls._getalias(aliases, tree) | ||
if r is None: | ||||
Augie Fackler
|
r43346 | return tuple( | ||
cls._expand(aliases, t, expanding, cache) for t in tree | ||||
) | ||||
Yuya Nishihara
|
r28909 | a, l = r | ||
Yuya Nishihara
|
r28896 | if a.error: | ||
raise error.Abort(a.error) | ||||
if a in expanding: | ||||
Augie Fackler
|
r43346 | raise error.ParseError( | ||
Martin von Zweigbergk
|
r43387 | _(b'infinite expansion of %(section)s "%(name)s" detected') | ||
Augie Fackler
|
r43347 | % {b'section': cls._section, b'name': a.name} | ||
Augie Fackler
|
r43346 | ) | ||
Yuya Nishihara
|
r28897 | # get cacheable replacement tree by expanding aliases recursively | ||
Yuya Nishihara
|
r28896 | expanding.append(a) | ||
if a.name not in cache: | ||||
Augie Fackler
|
r43346 | cache[a.name] = cls._expand( | ||
aliases, a.replacement, expanding, cache | ||||
) | ||||
Yuya Nishihara
|
r28896 | result = cache[a.name] | ||
expanding.pop() | ||||
if a.args is None: | ||||
return result | ||||
Yuya Nishihara
|
r28897 | # substitute function arguments in replacement tree | ||
Yuya Nishihara
|
r28896 | if len(l) != len(a.args): | ||
Augie Fackler
|
r43346 | raise error.ParseError( | ||
Augie Fackler
|
r43347 | _(b'invalid number of arguments: %d') % len(l) | ||
Augie Fackler
|
r43346 | ) | ||
Yuya Nishihara
|
r28896 | l = [cls._expand(aliases, t, [], cache) for t in l] | ||
return cls._expandargs(result, dict(zip(a.args, l))) | ||||
Yuya Nishihara
|
r28895 | |||
@classmethod | ||||
def expand(cls, aliases, tree): | ||||
"""Expand aliases in tree, recursively. | ||||
'aliases' is a dictionary mapping user defined aliases to alias objects. | ||||
""" | ||||
return cls._expand(aliases, tree, [], {}) | ||||