##// END OF EJS Templates
py3: byte-stringify ValueError of unescapestr() to reraise as ParseError
Yuya Nishihara -
r36565:7840d8bd default
parent child Browse files
Show More
@@ -1,700 +1,701 b''
1 1 # parser.py - simple top-down operator precedence parser for mercurial
2 2 #
3 3 # Copyright 2010 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 # see http://effbot.org/zone/simple-top-down-parsing.htm and
9 9 # http://eli.thegreenplace.net/2010/01/02/top-down-operator-precedence-parsing/
10 10 # for background
11 11
12 12 # takes a tokenizer and elements
13 13 # tokenizer is an iterator that returns (type, value, pos) tuples
14 14 # elements is a mapping of types to binding strength, primary, prefix, infix
15 15 # and suffix actions
16 16 # an action is a tree node name, a tree label, and an optional match
17 17 # __call__(program) parses program into a labeled tree
18 18
19 19 from __future__ import absolute_import, print_function
20 20
21 21 from .i18n import _
22 22 from . import (
23 23 encoding,
24 24 error,
25 pycompat,
25 26 util,
26 27 )
27 28
28 29 class parser(object):
29 30 def __init__(self, elements, methods=None):
30 31 self._elements = elements
31 32 self._methods = methods
32 33 self.current = None
33 34 def _advance(self):
34 35 'advance the tokenizer'
35 36 t = self.current
36 37 self.current = next(self._iter, None)
37 38 return t
38 39 def _hasnewterm(self):
39 40 'True if next token may start new term'
40 41 return any(self._elements[self.current[0]][1:3])
41 42 def _match(self, m):
42 43 'make sure the tokenizer matches an end condition'
43 44 if self.current[0] != m:
44 45 raise error.ParseError(_("unexpected token: %s") % self.current[0],
45 46 self.current[2])
46 47 self._advance()
47 48 def _parseoperand(self, bind, m=None):
48 49 'gather right-hand-side operand until an end condition or binding met'
49 50 if m and self.current[0] == m:
50 51 expr = None
51 52 else:
52 53 expr = self._parse(bind)
53 54 if m:
54 55 self._match(m)
55 56 return expr
56 57 def _parse(self, bind=0):
57 58 token, value, pos = self._advance()
58 59 # handle prefix rules on current token, take as primary if unambiguous
59 60 primary, prefix = self._elements[token][1:3]
60 61 if primary and not (prefix and self._hasnewterm()):
61 62 expr = (primary, value)
62 63 elif prefix:
63 64 expr = (prefix[0], self._parseoperand(*prefix[1:]))
64 65 else:
65 66 raise error.ParseError(_("not a prefix: %s") % token, pos)
66 67 # gather tokens until we meet a lower binding strength
67 68 while bind < self._elements[self.current[0]][0]:
68 69 token, value, pos = self._advance()
69 70 # handle infix rules, take as suffix if unambiguous
70 71 infix, suffix = self._elements[token][3:]
71 72 if suffix and not (infix and self._hasnewterm()):
72 73 expr = (suffix, expr)
73 74 elif infix:
74 75 expr = (infix[0], expr, self._parseoperand(*infix[1:]))
75 76 else:
76 77 raise error.ParseError(_("not an infix: %s") % token, pos)
77 78 return expr
78 79 def parse(self, tokeniter):
79 80 'generate a parse tree from tokens'
80 81 self._iter = tokeniter
81 82 self._advance()
82 83 res = self._parse()
83 84 token, value, pos = self.current
84 85 return res, pos
85 86 def eval(self, tree):
86 87 'recursively evaluate a parse tree using node methods'
87 88 if not isinstance(tree, tuple):
88 89 return tree
89 90 return self._methods[tree[0]](*[self.eval(t) for t in tree[1:]])
90 91 def __call__(self, tokeniter):
91 92 'parse tokens into a parse tree and evaluate if methods given'
92 93 t = self.parse(tokeniter)
93 94 if self._methods:
94 95 return self.eval(t)
95 96 return t
96 97
97 98 def splitargspec(spec):
98 99 """Parse spec of function arguments into (poskeys, varkey, keys, optkey)
99 100
100 101 >>> splitargspec(b'')
101 102 ([], None, [], None)
102 103 >>> splitargspec(b'foo bar')
103 104 ([], None, ['foo', 'bar'], None)
104 105 >>> splitargspec(b'foo *bar baz **qux')
105 106 (['foo'], 'bar', ['baz'], 'qux')
106 107 >>> splitargspec(b'*foo')
107 108 ([], 'foo', [], None)
108 109 >>> splitargspec(b'**foo')
109 110 ([], None, [], 'foo')
110 111 """
111 112 optkey = None
112 113 pre, sep, post = spec.partition('**')
113 114 if sep:
114 115 posts = post.split()
115 116 if not posts:
116 117 raise error.ProgrammingError('no **optkey name provided')
117 118 if len(posts) > 1:
118 119 raise error.ProgrammingError('excessive **optkey names provided')
119 120 optkey = posts[0]
120 121
121 122 pre, sep, post = pre.partition('*')
122 123 pres = pre.split()
123 124 posts = post.split()
124 125 if sep:
125 126 if not posts:
126 127 raise error.ProgrammingError('no *varkey name provided')
127 128 return pres, posts[0], posts[1:], optkey
128 129 return [], None, pres, optkey
129 130
130 131 def buildargsdict(trees, funcname, argspec, keyvaluenode, keynode):
131 132 """Build dict from list containing positional and keyword arguments
132 133
133 134 Arguments are specified by a tuple of ``(poskeys, varkey, keys, optkey)``
134 135 where
135 136
136 137 - ``poskeys``: list of names of positional arguments
137 138 - ``varkey``: optional argument name that takes up remainder
138 139 - ``keys``: list of names that can be either positional or keyword arguments
139 140 - ``optkey``: optional argument name that takes up excess keyword arguments
140 141
141 142 If ``varkey`` specified, all ``keys`` must be given as keyword arguments.
142 143
143 144 Invalid keywords, too few positional arguments, or too many positional
144 145 arguments are rejected, but missing keyword arguments are just omitted.
145 146 """
146 147 poskeys, varkey, keys, optkey = argspec
147 148 kwstart = next((i for i, x in enumerate(trees) if x[0] == keyvaluenode),
148 149 len(trees))
149 150 if kwstart < len(poskeys):
150 151 raise error.ParseError(_("%(func)s takes at least %(nargs)d positional "
151 152 "arguments")
152 153 % {'func': funcname, 'nargs': len(poskeys)})
153 154 if not varkey and kwstart > len(poskeys) + len(keys):
154 155 raise error.ParseError(_("%(func)s takes at most %(nargs)d positional "
155 156 "arguments")
156 157 % {'func': funcname,
157 158 'nargs': len(poskeys) + len(keys)})
158 159 args = util.sortdict()
159 160 # consume positional arguments
160 161 for k, x in zip(poskeys, trees[:kwstart]):
161 162 args[k] = x
162 163 if varkey:
163 164 args[varkey] = trees[len(args):kwstart]
164 165 else:
165 166 for k, x in zip(keys, trees[len(args):kwstart]):
166 167 args[k] = x
167 168 # remainder should be keyword arguments
168 169 if optkey:
169 170 args[optkey] = util.sortdict()
170 171 for x in trees[kwstart:]:
171 172 if x[0] != keyvaluenode or x[1][0] != keynode:
172 173 raise error.ParseError(_("%(func)s got an invalid argument")
173 174 % {'func': funcname})
174 175 k = x[1][1]
175 176 if k in keys:
176 177 d = args
177 178 elif not optkey:
178 179 raise error.ParseError(_("%(func)s got an unexpected keyword "
179 180 "argument '%(key)s'")
180 181 % {'func': funcname, 'key': k})
181 182 else:
182 183 d = args[optkey]
183 184 if k in d:
184 185 raise error.ParseError(_("%(func)s got multiple values for keyword "
185 186 "argument '%(key)s'")
186 187 % {'func': funcname, 'key': k})
187 188 d[k] = x[2]
188 189 return args
189 190
190 191 def unescapestr(s):
191 192 try:
192 193 return util.unescapestr(s)
193 194 except ValueError as e:
194 195 # mangle Python's exception into our format
195 raise error.ParseError(str(e).lower())
196 raise error.ParseError(pycompat.bytestr(e).lower())
196 197
197 198 def _brepr(obj):
198 199 if isinstance(obj, bytes):
199 200 return b"'%s'" % util.escapestr(obj)
200 201 return encoding.strtolocal(repr(obj))
201 202
202 203 def _prettyformat(tree, leafnodes, level, lines):
203 204 if not isinstance(tree, tuple):
204 205 lines.append((level, _brepr(tree)))
205 206 elif tree[0] in leafnodes:
206 207 rs = map(_brepr, tree[1:])
207 208 lines.append((level, '(%s %s)' % (tree[0], ' '.join(rs))))
208 209 else:
209 210 lines.append((level, '(%s' % tree[0]))
210 211 for s in tree[1:]:
211 212 _prettyformat(s, leafnodes, level + 1, lines)
212 213 lines[-1:] = [(lines[-1][0], lines[-1][1] + ')')]
213 214
214 215 def prettyformat(tree, leafnodes):
215 216 lines = []
216 217 _prettyformat(tree, leafnodes, 0, lines)
217 218 output = '\n'.join((' ' * l + s) for l, s in lines)
218 219 return output
219 220
220 221 def simplifyinfixops(tree, targetnodes):
221 222 """Flatten chained infix operations to reduce usage of Python stack
222 223
223 224 >>> from . import pycompat
224 225 >>> def f(tree):
225 226 ... s = prettyformat(simplifyinfixops(tree, (b'or',)), (b'symbol',))
226 227 ... print(pycompat.sysstr(s))
227 228 >>> f((b'or',
228 229 ... (b'or',
229 230 ... (b'symbol', b'1'),
230 231 ... (b'symbol', b'2')),
231 232 ... (b'symbol', b'3')))
232 233 (or
233 234 (symbol '1')
234 235 (symbol '2')
235 236 (symbol '3'))
236 237 >>> f((b'func',
237 238 ... (b'symbol', b'p1'),
238 239 ... (b'or',
239 240 ... (b'or',
240 241 ... (b'func',
241 242 ... (b'symbol', b'sort'),
242 243 ... (b'list',
243 244 ... (b'or',
244 245 ... (b'or',
245 246 ... (b'symbol', b'1'),
246 247 ... (b'symbol', b'2')),
247 248 ... (b'symbol', b'3')),
248 249 ... (b'negate',
249 250 ... (b'symbol', b'rev')))),
250 251 ... (b'and',
251 252 ... (b'symbol', b'4'),
252 253 ... (b'group',
253 254 ... (b'or',
254 255 ... (b'or',
255 256 ... (b'symbol', b'5'),
256 257 ... (b'symbol', b'6')),
257 258 ... (b'symbol', b'7'))))),
258 259 ... (b'symbol', b'8'))))
259 260 (func
260 261 (symbol 'p1')
261 262 (or
262 263 (func
263 264 (symbol 'sort')
264 265 (list
265 266 (or
266 267 (symbol '1')
267 268 (symbol '2')
268 269 (symbol '3'))
269 270 (negate
270 271 (symbol 'rev'))))
271 272 (and
272 273 (symbol '4')
273 274 (group
274 275 (or
275 276 (symbol '5')
276 277 (symbol '6')
277 278 (symbol '7'))))
278 279 (symbol '8')))
279 280 """
280 281 if not isinstance(tree, tuple):
281 282 return tree
282 283 op = tree[0]
283 284 if op not in targetnodes:
284 285 return (op,) + tuple(simplifyinfixops(x, targetnodes) for x in tree[1:])
285 286
286 287 # walk down left nodes taking each right node. no recursion to left nodes
287 288 # because infix operators are left-associative, i.e. left tree is deep.
288 289 # e.g. '1 + 2 + 3' -> (+ (+ 1 2) 3) -> (+ 1 2 3)
289 290 simplified = []
290 291 x = tree
291 292 while x[0] == op:
292 293 l, r = x[1:]
293 294 simplified.append(simplifyinfixops(r, targetnodes))
294 295 x = l
295 296 simplified.append(simplifyinfixops(x, targetnodes))
296 297 simplified.append(op)
297 298 return tuple(reversed(simplified))
298 299
299 300 def _buildtree(template, placeholder, replstack):
300 301 if template == placeholder:
301 302 return replstack.pop()
302 303 if not isinstance(template, tuple):
303 304 return template
304 305 return tuple(_buildtree(x, placeholder, replstack) for x in template)
305 306
306 307 def buildtree(template, placeholder, *repls):
307 308 """Create new tree by substituting placeholders by replacements
308 309
309 310 >>> _ = (b'symbol', b'_')
310 311 >>> def f(template, *repls):
311 312 ... return buildtree(template, _, *repls)
312 313 >>> f((b'func', (b'symbol', b'only'), (b'list', _, _)),
313 314 ... ('symbol', '1'), ('symbol', '2'))
314 315 ('func', ('symbol', 'only'), ('list', ('symbol', '1'), ('symbol', '2')))
315 316 >>> f((b'and', _, (b'not', _)), (b'symbol', b'1'), (b'symbol', b'2'))
316 317 ('and', ('symbol', '1'), ('not', ('symbol', '2')))
317 318 """
318 319 if not isinstance(placeholder, tuple):
319 320 raise error.ProgrammingError('placeholder must be a node tuple')
320 321 replstack = list(reversed(repls))
321 322 r = _buildtree(template, placeholder, replstack)
322 323 if replstack:
323 324 raise error.ProgrammingError('too many replacements')
324 325 return r
325 326
326 327 def _matchtree(pattern, tree, placeholder, incompletenodes, matches):
327 328 if pattern == tree:
328 329 return True
329 330 if not isinstance(pattern, tuple) or not isinstance(tree, tuple):
330 331 return False
331 332 if pattern == placeholder and tree[0] not in incompletenodes:
332 333 matches.append(tree)
333 334 return True
334 335 if len(pattern) != len(tree):
335 336 return False
336 337 return all(_matchtree(p, x, placeholder, incompletenodes, matches)
337 338 for p, x in zip(pattern, tree))
338 339
339 340 def matchtree(pattern, tree, placeholder=None, incompletenodes=()):
340 341 """If a tree matches the pattern, return a list of the tree and nodes
341 342 matched with the placeholder; Otherwise None
342 343
343 344 >>> def f(pattern, tree):
344 345 ... m = matchtree(pattern, tree, _, {b'keyvalue', b'list'})
345 346 ... if m:
346 347 ... return m[1:]
347 348
348 349 >>> _ = (b'symbol', b'_')
349 350 >>> f((b'func', (b'symbol', b'ancestors'), _),
350 351 ... (b'func', (b'symbol', b'ancestors'), (b'symbol', b'1')))
351 352 [('symbol', '1')]
352 353 >>> f((b'func', (b'symbol', b'ancestors'), _),
353 354 ... (b'func', (b'symbol', b'ancestors'), None))
354 355 >>> f((b'range', (b'dagrange', _, _), _),
355 356 ... (b'range',
356 357 ... (b'dagrange', (b'symbol', b'1'), (b'symbol', b'2')),
357 358 ... (b'symbol', b'3')))
358 359 [('symbol', '1'), ('symbol', '2'), ('symbol', '3')]
359 360
360 361 The placeholder does not match the specified incomplete nodes because
361 362 an incomplete node (e.g. argument list) cannot construct an expression.
362 363
363 364 >>> f((b'func', (b'symbol', b'ancestors'), _),
364 365 ... (b'func', (b'symbol', b'ancestors'),
365 366 ... (b'list', (b'symbol', b'1'), (b'symbol', b'2'))))
366 367
367 368 The placeholder may be omitted, but which shouldn't match a None node.
368 369
369 370 >>> _ = None
370 371 >>> f((b'func', (b'symbol', b'ancestors'), None),
371 372 ... (b'func', (b'symbol', b'ancestors'), (b'symbol', b'0')))
372 373 """
373 374 if placeholder is not None and not isinstance(placeholder, tuple):
374 375 raise error.ProgrammingError('placeholder must be a node tuple')
375 376 matches = [tree]
376 377 if _matchtree(pattern, tree, placeholder, incompletenodes, matches):
377 378 return matches
378 379
379 380 def parseerrordetail(inst):
380 381 """Compose error message from specified ParseError object
381 382 """
382 383 if len(inst.args) > 1:
383 384 return _('at %d: %s') % (inst.args[1], inst.args[0])
384 385 else:
385 386 return inst.args[0]
386 387
387 388 class alias(object):
388 389 """Parsed result of alias"""
389 390
390 391 def __init__(self, name, args, err, replacement):
391 392 self.name = name
392 393 self.args = args
393 394 self.error = err
394 395 self.replacement = replacement
395 396 # whether own `error` information is already shown or not.
396 397 # this avoids showing same warning multiple times at each
397 398 # `expandaliases`.
398 399 self.warned = False
399 400
400 401 class basealiasrules(object):
401 402 """Parsing and expansion rule set of aliases
402 403
403 404 This is a helper for fileset/revset/template aliases. A concrete rule set
404 405 should be made by sub-classing this and implementing class/static methods.
405 406
406 407 It supports alias expansion of symbol and function-call styles::
407 408
408 409 # decl = defn
409 410 h = heads(default)
410 411 b($1) = ancestors($1) - ancestors(default)
411 412 """
412 413 # typically a config section, which will be included in error messages
413 414 _section = None
414 415 # tag of symbol node
415 416 _symbolnode = 'symbol'
416 417
417 418 def __new__(cls):
418 419 raise TypeError("'%s' is not instantiatable" % cls.__name__)
419 420
420 421 @staticmethod
421 422 def _parse(spec):
422 423 """Parse an alias name, arguments and definition"""
423 424 raise NotImplementedError
424 425
425 426 @staticmethod
426 427 def _trygetfunc(tree):
427 428 """Return (name, args) if tree is a function; otherwise None"""
428 429 raise NotImplementedError
429 430
430 431 @classmethod
431 432 def _builddecl(cls, decl):
432 433 """Parse an alias declaration into ``(name, args, errorstr)``
433 434
434 435 This function analyzes the parsed tree. The parsing rule is provided
435 436 by ``_parse()``.
436 437
437 438 - ``name``: of declared alias (may be ``decl`` itself at error)
438 439 - ``args``: list of argument names (or None for symbol declaration)
439 440 - ``errorstr``: detail about detected error (or None)
440 441
441 442 >>> sym = lambda x: (b'symbol', x)
442 443 >>> symlist = lambda *xs: (b'list',) + tuple(sym(x) for x in xs)
443 444 >>> func = lambda n, a: (b'func', sym(n), a)
444 445 >>> parsemap = {
445 446 ... b'foo': sym(b'foo'),
446 447 ... b'$foo': sym(b'$foo'),
447 448 ... b'foo::bar': (b'dagrange', sym(b'foo'), sym(b'bar')),
448 449 ... b'foo()': func(b'foo', None),
449 450 ... b'$foo()': func(b'$foo', None),
450 451 ... b'foo($1, $2)': func(b'foo', symlist(b'$1', b'$2')),
451 452 ... b'foo(bar_bar, baz.baz)':
452 453 ... func(b'foo', symlist(b'bar_bar', b'baz.baz')),
453 454 ... b'foo(bar($1, $2))':
454 455 ... func(b'foo', func(b'bar', symlist(b'$1', b'$2'))),
455 456 ... b'foo($1, $2, nested($1, $2))':
456 457 ... func(b'foo', (symlist(b'$1', b'$2') +
457 458 ... (func(b'nested', symlist(b'$1', b'$2')),))),
458 459 ... b'foo("bar")': func(b'foo', (b'string', b'bar')),
459 460 ... b'foo($1, $2': error.ParseError(b'unexpected token: end', 10),
460 461 ... b'foo("bar': error.ParseError(b'unterminated string', 5),
461 462 ... b'foo($1, $2, $1)': func(b'foo', symlist(b'$1', b'$2', b'$1')),
462 463 ... }
463 464 >>> def parse(expr):
464 465 ... x = parsemap[expr]
465 466 ... if isinstance(x, Exception):
466 467 ... raise x
467 468 ... return x
468 469 >>> def trygetfunc(tree):
469 470 ... if not tree or tree[0] != b'func' or tree[1][0] != b'symbol':
470 471 ... return None
471 472 ... if not tree[2]:
472 473 ... return tree[1][1], []
473 474 ... if tree[2][0] == b'list':
474 475 ... return tree[1][1], list(tree[2][1:])
475 476 ... return tree[1][1], [tree[2]]
476 477 >>> class aliasrules(basealiasrules):
477 478 ... _parse = staticmethod(parse)
478 479 ... _trygetfunc = staticmethod(trygetfunc)
479 480 >>> builddecl = aliasrules._builddecl
480 481 >>> builddecl(b'foo')
481 482 ('foo', None, None)
482 483 >>> builddecl(b'$foo')
483 484 ('$foo', None, "invalid symbol '$foo'")
484 485 >>> builddecl(b'foo::bar')
485 486 ('foo::bar', None, 'invalid format')
486 487 >>> builddecl(b'foo()')
487 488 ('foo', [], None)
488 489 >>> builddecl(b'$foo()')
489 490 ('$foo()', None, "invalid function '$foo'")
490 491 >>> builddecl(b'foo($1, $2)')
491 492 ('foo', ['$1', '$2'], None)
492 493 >>> builddecl(b'foo(bar_bar, baz.baz)')
493 494 ('foo', ['bar_bar', 'baz.baz'], None)
494 495 >>> builddecl(b'foo($1, $2, nested($1, $2))')
495 496 ('foo($1, $2, nested($1, $2))', None, 'invalid argument list')
496 497 >>> builddecl(b'foo(bar($1, $2))')
497 498 ('foo(bar($1, $2))', None, 'invalid argument list')
498 499 >>> builddecl(b'foo("bar")')
499 500 ('foo("bar")', None, 'invalid argument list')
500 501 >>> builddecl(b'foo($1, $2')
501 502 ('foo($1, $2', None, 'at 10: unexpected token: end')
502 503 >>> builddecl(b'foo("bar')
503 504 ('foo("bar', None, 'at 5: unterminated string')
504 505 >>> builddecl(b'foo($1, $2, $1)')
505 506 ('foo', None, 'argument names collide with each other')
506 507 """
507 508 try:
508 509 tree = cls._parse(decl)
509 510 except error.ParseError as inst:
510 511 return (decl, None, parseerrordetail(inst))
511 512
512 513 if tree[0] == cls._symbolnode:
513 514 # "name = ...." style
514 515 name = tree[1]
515 516 if name.startswith('$'):
516 517 return (decl, None, _("invalid symbol '%s'") % name)
517 518 return (name, None, None)
518 519
519 520 func = cls._trygetfunc(tree)
520 521 if func:
521 522 # "name(arg, ....) = ...." style
522 523 name, args = func
523 524 if name.startswith('$'):
524 525 return (decl, None, _("invalid function '%s'") % name)
525 526 if any(t[0] != cls._symbolnode for t in args):
526 527 return (decl, None, _("invalid argument list"))
527 528 if len(args) != len(set(args)):
528 529 return (name, None, _("argument names collide with each other"))
529 530 return (name, [t[1] for t in args], None)
530 531
531 532 return (decl, None, _("invalid format"))
532 533
533 534 @classmethod
534 535 def _relabelargs(cls, tree, args):
535 536 """Mark alias arguments as ``_aliasarg``"""
536 537 if not isinstance(tree, tuple):
537 538 return tree
538 539 op = tree[0]
539 540 if op != cls._symbolnode:
540 541 return (op,) + tuple(cls._relabelargs(x, args) for x in tree[1:])
541 542
542 543 assert len(tree) == 2
543 544 sym = tree[1]
544 545 if sym in args:
545 546 op = '_aliasarg'
546 547 elif sym.startswith('$'):
547 548 raise error.ParseError(_("invalid symbol '%s'") % sym)
548 549 return (op, sym)
549 550
550 551 @classmethod
551 552 def _builddefn(cls, defn, args):
552 553 """Parse an alias definition into a tree and marks substitutions
553 554
554 555 This function marks alias argument references as ``_aliasarg``. The
555 556 parsing rule is provided by ``_parse()``.
556 557
557 558 ``args`` is a list of alias argument names, or None if the alias
558 559 is declared as a symbol.
559 560
560 561 >>> from . import pycompat
561 562 >>> parsemap = {
562 563 ... b'$1 or foo': (b'or', (b'symbol', b'$1'), (b'symbol', b'foo')),
563 564 ... b'$1 or $bar':
564 565 ... (b'or', (b'symbol', b'$1'), (b'symbol', b'$bar')),
565 566 ... b'$10 or baz':
566 567 ... (b'or', (b'symbol', b'$10'), (b'symbol', b'baz')),
567 568 ... b'"$1" or "foo"':
568 569 ... (b'or', (b'string', b'$1'), (b'string', b'foo')),
569 570 ... }
570 571 >>> class aliasrules(basealiasrules):
571 572 ... _parse = staticmethod(parsemap.__getitem__)
572 573 ... _trygetfunc = staticmethod(lambda x: None)
573 574 >>> builddefn = aliasrules._builddefn
574 575 >>> def pprint(tree):
575 576 ... s = prettyformat(tree, (b'_aliasarg', b'string', b'symbol'))
576 577 ... print(pycompat.sysstr(s))
577 578 >>> args = [b'$1', b'$2', b'foo']
578 579 >>> pprint(builddefn(b'$1 or foo', args))
579 580 (or
580 581 (_aliasarg '$1')
581 582 (_aliasarg 'foo'))
582 583 >>> try:
583 584 ... builddefn(b'$1 or $bar', args)
584 585 ... except error.ParseError as inst:
585 586 ... print(pycompat.sysstr(parseerrordetail(inst)))
586 587 invalid symbol '$bar'
587 588 >>> args = [b'$1', b'$10', b'foo']
588 589 >>> pprint(builddefn(b'$10 or baz', args))
589 590 (or
590 591 (_aliasarg '$10')
591 592 (symbol 'baz'))
592 593 >>> pprint(builddefn(b'"$1" or "foo"', args))
593 594 (or
594 595 (string '$1')
595 596 (string 'foo'))
596 597 """
597 598 tree = cls._parse(defn)
598 599 if args:
599 600 args = set(args)
600 601 else:
601 602 args = set()
602 603 return cls._relabelargs(tree, args)
603 604
604 605 @classmethod
605 606 def build(cls, decl, defn):
606 607 """Parse an alias declaration and definition into an alias object"""
607 608 repl = efmt = None
608 609 name, args, err = cls._builddecl(decl)
609 610 if err:
610 611 efmt = _('bad declaration of %(section)s "%(name)s": %(error)s')
611 612 else:
612 613 try:
613 614 repl = cls._builddefn(defn, args)
614 615 except error.ParseError as inst:
615 616 err = parseerrordetail(inst)
616 617 efmt = _('bad definition of %(section)s "%(name)s": %(error)s')
617 618 if err:
618 619 err = efmt % {'section': cls._section, 'name': name, 'error': err}
619 620 return alias(name, args, err, repl)
620 621
621 622 @classmethod
622 623 def buildmap(cls, items):
623 624 """Parse a list of alias (name, replacement) pairs into a dict of
624 625 alias objects"""
625 626 aliases = {}
626 627 for decl, defn in items:
627 628 a = cls.build(decl, defn)
628 629 aliases[a.name] = a
629 630 return aliases
630 631
631 632 @classmethod
632 633 def _getalias(cls, aliases, tree):
633 634 """If tree looks like an unexpanded alias, return (alias, pattern-args)
634 635 pair. Return None otherwise.
635 636 """
636 637 if not isinstance(tree, tuple):
637 638 return None
638 639 if tree[0] == cls._symbolnode:
639 640 name = tree[1]
640 641 a = aliases.get(name)
641 642 if a and a.args is None:
642 643 return a, None
643 644 func = cls._trygetfunc(tree)
644 645 if func:
645 646 name, args = func
646 647 a = aliases.get(name)
647 648 if a and a.args is not None:
648 649 return a, args
649 650 return None
650 651
651 652 @classmethod
652 653 def _expandargs(cls, tree, args):
653 654 """Replace _aliasarg instances with the substitution value of the
654 655 same name in args, recursively.
655 656 """
656 657 if not isinstance(tree, tuple):
657 658 return tree
658 659 if tree[0] == '_aliasarg':
659 660 sym = tree[1]
660 661 return args[sym]
661 662 return tuple(cls._expandargs(t, args) for t in tree)
662 663
663 664 @classmethod
664 665 def _expand(cls, aliases, tree, expanding, cache):
665 666 if not isinstance(tree, tuple):
666 667 return tree
667 668 r = cls._getalias(aliases, tree)
668 669 if r is None:
669 670 return tuple(cls._expand(aliases, t, expanding, cache)
670 671 for t in tree)
671 672 a, l = r
672 673 if a.error:
673 674 raise error.Abort(a.error)
674 675 if a in expanding:
675 676 raise error.ParseError(_('infinite expansion of %(section)s '
676 677 '"%(name)s" detected')
677 678 % {'section': cls._section, 'name': a.name})
678 679 # get cacheable replacement tree by expanding aliases recursively
679 680 expanding.append(a)
680 681 if a.name not in cache:
681 682 cache[a.name] = cls._expand(aliases, a.replacement, expanding,
682 683 cache)
683 684 result = cache[a.name]
684 685 expanding.pop()
685 686 if a.args is None:
686 687 return result
687 688 # substitute function arguments in replacement tree
688 689 if len(l) != len(a.args):
689 690 raise error.ParseError(_('invalid number of arguments: %d')
690 691 % len(l))
691 692 l = [cls._expand(aliases, t, [], cache) for t in l]
692 693 return cls._expandargs(result, dict(zip(a.args, l)))
693 694
694 695 @classmethod
695 696 def expand(cls, aliases, tree):
696 697 """Expand aliases in tree, recursively.
697 698
698 699 'aliases' is a dictionary mapping user defined aliases to alias objects.
699 700 """
700 701 return cls._expand(aliases, tree, [], {})
General Comments 0
You need to be logged in to leave comments. Login now