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