##// END OF EJS Templates
revsetlang: perform quoting using ui.escapestr instead of repr()...
Augie Fackler -
r31606:0b3eb280 default
parent child Browse files
Show More
@@ -1,701 +1,702 b''
1 # revsetlang.py - parser, tokenizer and utility for revision set language
1 # revsetlang.py - parser, tokenizer and utility for revision set language
2 #
2 #
3 # Copyright 2010 Matt Mackall <mpm@selenic.com>
3 # Copyright 2010 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import string
10 import string
11
11
12 from .i18n import _
12 from .i18n import _
13 from . import (
13 from . import (
14 error,
14 error,
15 node,
15 node,
16 parser,
16 parser,
17 pycompat,
17 pycompat,
18 util,
18 )
19 )
19
20
20 elements = {
21 elements = {
21 # token-type: binding-strength, primary, prefix, infix, suffix
22 # token-type: binding-strength, primary, prefix, infix, suffix
22 "(": (21, None, ("group", 1, ")"), ("func", 1, ")"), None),
23 "(": (21, None, ("group", 1, ")"), ("func", 1, ")"), None),
23 "##": (20, None, None, ("_concat", 20), None),
24 "##": (20, None, None, ("_concat", 20), None),
24 "~": (18, None, None, ("ancestor", 18), None),
25 "~": (18, None, None, ("ancestor", 18), None),
25 "^": (18, None, None, ("parent", 18), "parentpost"),
26 "^": (18, None, None, ("parent", 18), "parentpost"),
26 "-": (5, None, ("negate", 19), ("minus", 5), None),
27 "-": (5, None, ("negate", 19), ("minus", 5), None),
27 "::": (17, None, ("dagrangepre", 17), ("dagrange", 17), "dagrangepost"),
28 "::": (17, None, ("dagrangepre", 17), ("dagrange", 17), "dagrangepost"),
28 "..": (17, None, ("dagrangepre", 17), ("dagrange", 17), "dagrangepost"),
29 "..": (17, None, ("dagrangepre", 17), ("dagrange", 17), "dagrangepost"),
29 ":": (15, "rangeall", ("rangepre", 15), ("range", 15), "rangepost"),
30 ":": (15, "rangeall", ("rangepre", 15), ("range", 15), "rangepost"),
30 "not": (10, None, ("not", 10), None, None),
31 "not": (10, None, ("not", 10), None, None),
31 "!": (10, None, ("not", 10), None, None),
32 "!": (10, None, ("not", 10), None, None),
32 "and": (5, None, None, ("and", 5), None),
33 "and": (5, None, None, ("and", 5), None),
33 "&": (5, None, None, ("and", 5), None),
34 "&": (5, None, None, ("and", 5), None),
34 "%": (5, None, None, ("only", 5), "onlypost"),
35 "%": (5, None, None, ("only", 5), "onlypost"),
35 "or": (4, None, None, ("or", 4), None),
36 "or": (4, None, None, ("or", 4), None),
36 "|": (4, None, None, ("or", 4), None),
37 "|": (4, None, None, ("or", 4), None),
37 "+": (4, None, None, ("or", 4), None),
38 "+": (4, None, None, ("or", 4), None),
38 "=": (3, None, None, ("keyvalue", 3), None),
39 "=": (3, None, None, ("keyvalue", 3), None),
39 ",": (2, None, None, ("list", 2), None),
40 ",": (2, None, None, ("list", 2), None),
40 ")": (0, None, None, None, None),
41 ")": (0, None, None, None, None),
41 "symbol": (0, "symbol", None, None, None),
42 "symbol": (0, "symbol", None, None, None),
42 "string": (0, "string", None, None, None),
43 "string": (0, "string", None, None, None),
43 "end": (0, None, None, None, None),
44 "end": (0, None, None, None, None),
44 }
45 }
45
46
46 keywords = set(['and', 'or', 'not'])
47 keywords = set(['and', 'or', 'not'])
47
48
48 _quoteletters = set(['"', "'"])
49 _quoteletters = set(['"', "'"])
49 _simpleopletters = set(pycompat.iterbytestr("():=,-|&+!~^%"))
50 _simpleopletters = set(pycompat.iterbytestr("():=,-|&+!~^%"))
50
51
51 # default set of valid characters for the initial letter of symbols
52 # default set of valid characters for the initial letter of symbols
52 _syminitletters = set(pycompat.iterbytestr(
53 _syminitletters = set(pycompat.iterbytestr(
53 string.ascii_letters.encode('ascii') +
54 string.ascii_letters.encode('ascii') +
54 string.digits.encode('ascii') +
55 string.digits.encode('ascii') +
55 '._@')) | set(map(pycompat.bytechr, xrange(128, 256)))
56 '._@')) | set(map(pycompat.bytechr, xrange(128, 256)))
56
57
57 # default set of valid characters for non-initial letters of symbols
58 # default set of valid characters for non-initial letters of symbols
58 _symletters = _syminitletters | set(pycompat.iterbytestr('-/'))
59 _symletters = _syminitletters | set(pycompat.iterbytestr('-/'))
59
60
60 def tokenize(program, lookup=None, syminitletters=None, symletters=None):
61 def tokenize(program, lookup=None, syminitletters=None, symletters=None):
61 '''
62 '''
62 Parse a revset statement into a stream of tokens
63 Parse a revset statement into a stream of tokens
63
64
64 ``syminitletters`` is the set of valid characters for the initial
65 ``syminitletters`` is the set of valid characters for the initial
65 letter of symbols.
66 letter of symbols.
66
67
67 By default, character ``c`` is recognized as valid for initial
68 By default, character ``c`` is recognized as valid for initial
68 letter of symbols, if ``c.isalnum() or c in '._@' or ord(c) > 127``.
69 letter of symbols, if ``c.isalnum() or c in '._@' or ord(c) > 127``.
69
70
70 ``symletters`` is the set of valid characters for non-initial
71 ``symletters`` is the set of valid characters for non-initial
71 letters of symbols.
72 letters of symbols.
72
73
73 By default, character ``c`` is recognized as valid for non-initial
74 By default, character ``c`` is recognized as valid for non-initial
74 letters of symbols, if ``c.isalnum() or c in '-._/@' or ord(c) > 127``.
75 letters of symbols, if ``c.isalnum() or c in '-._/@' or ord(c) > 127``.
75
76
76 Check that @ is a valid unquoted token character (issue3686):
77 Check that @ is a valid unquoted token character (issue3686):
77 >>> list(tokenize("@::"))
78 >>> list(tokenize("@::"))
78 [('symbol', '@', 0), ('::', None, 1), ('end', None, 3)]
79 [('symbol', '@', 0), ('::', None, 1), ('end', None, 3)]
79
80
80 '''
81 '''
81 program = pycompat.bytestr(program)
82 program = pycompat.bytestr(program)
82 if syminitletters is None:
83 if syminitletters is None:
83 syminitletters = _syminitletters
84 syminitletters = _syminitletters
84 if symletters is None:
85 if symletters is None:
85 symletters = _symletters
86 symletters = _symletters
86
87
87 if program and lookup:
88 if program and lookup:
88 # attempt to parse old-style ranges first to deal with
89 # attempt to parse old-style ranges first to deal with
89 # things like old-tag which contain query metacharacters
90 # things like old-tag which contain query metacharacters
90 parts = program.split(':', 1)
91 parts = program.split(':', 1)
91 if all(lookup(sym) for sym in parts if sym):
92 if all(lookup(sym) for sym in parts if sym):
92 if parts[0]:
93 if parts[0]:
93 yield ('symbol', parts[0], 0)
94 yield ('symbol', parts[0], 0)
94 if len(parts) > 1:
95 if len(parts) > 1:
95 s = len(parts[0])
96 s = len(parts[0])
96 yield (':', None, s)
97 yield (':', None, s)
97 if parts[1]:
98 if parts[1]:
98 yield ('symbol', parts[1], s + 1)
99 yield ('symbol', parts[1], s + 1)
99 yield ('end', None, len(program))
100 yield ('end', None, len(program))
100 return
101 return
101
102
102 pos, l = 0, len(program)
103 pos, l = 0, len(program)
103 while pos < l:
104 while pos < l:
104 c = program[pos]
105 c = program[pos]
105 if c.isspace(): # skip inter-token whitespace
106 if c.isspace(): # skip inter-token whitespace
106 pass
107 pass
107 elif c == ':' and program[pos:pos + 2] == '::': # look ahead carefully
108 elif c == ':' and program[pos:pos + 2] == '::': # look ahead carefully
108 yield ('::', None, pos)
109 yield ('::', None, pos)
109 pos += 1 # skip ahead
110 pos += 1 # skip ahead
110 elif c == '.' and program[pos:pos + 2] == '..': # look ahead carefully
111 elif c == '.' and program[pos:pos + 2] == '..': # look ahead carefully
111 yield ('..', None, pos)
112 yield ('..', None, pos)
112 pos += 1 # skip ahead
113 pos += 1 # skip ahead
113 elif c == '#' and program[pos:pos + 2] == '##': # look ahead carefully
114 elif c == '#' and program[pos:pos + 2] == '##': # look ahead carefully
114 yield ('##', None, pos)
115 yield ('##', None, pos)
115 pos += 1 # skip ahead
116 pos += 1 # skip ahead
116 elif c in _simpleopletters: # handle simple operators
117 elif c in _simpleopletters: # handle simple operators
117 yield (c, None, pos)
118 yield (c, None, pos)
118 elif (c in _quoteletters or c == 'r' and
119 elif (c in _quoteletters or c == 'r' and
119 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
120 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
120 if c == 'r':
121 if c == 'r':
121 pos += 1
122 pos += 1
122 c = program[pos]
123 c = program[pos]
123 decode = lambda x: x
124 decode = lambda x: x
124 else:
125 else:
125 decode = parser.unescapestr
126 decode = parser.unescapestr
126 pos += 1
127 pos += 1
127 s = pos
128 s = pos
128 while pos < l: # find closing quote
129 while pos < l: # find closing quote
129 d = program[pos]
130 d = program[pos]
130 if d == '\\': # skip over escaped characters
131 if d == '\\': # skip over escaped characters
131 pos += 2
132 pos += 2
132 continue
133 continue
133 if d == c:
134 if d == c:
134 yield ('string', decode(program[s:pos]), s)
135 yield ('string', decode(program[s:pos]), s)
135 break
136 break
136 pos += 1
137 pos += 1
137 else:
138 else:
138 raise error.ParseError(_("unterminated string"), s)
139 raise error.ParseError(_("unterminated string"), s)
139 # gather up a symbol/keyword
140 # gather up a symbol/keyword
140 elif c in syminitletters:
141 elif c in syminitletters:
141 s = pos
142 s = pos
142 pos += 1
143 pos += 1
143 while pos < l: # find end of symbol
144 while pos < l: # find end of symbol
144 d = program[pos]
145 d = program[pos]
145 if d not in symletters:
146 if d not in symletters:
146 break
147 break
147 if d == '.' and program[pos - 1] == '.': # special case for ..
148 if d == '.' and program[pos - 1] == '.': # special case for ..
148 pos -= 1
149 pos -= 1
149 break
150 break
150 pos += 1
151 pos += 1
151 sym = program[s:pos]
152 sym = program[s:pos]
152 if sym in keywords: # operator keywords
153 if sym in keywords: # operator keywords
153 yield (sym, None, s)
154 yield (sym, None, s)
154 elif '-' in sym:
155 elif '-' in sym:
155 # some jerk gave us foo-bar-baz, try to check if it's a symbol
156 # some jerk gave us foo-bar-baz, try to check if it's a symbol
156 if lookup and lookup(sym):
157 if lookup and lookup(sym):
157 # looks like a real symbol
158 # looks like a real symbol
158 yield ('symbol', sym, s)
159 yield ('symbol', sym, s)
159 else:
160 else:
160 # looks like an expression
161 # looks like an expression
161 parts = sym.split('-')
162 parts = sym.split('-')
162 for p in parts[:-1]:
163 for p in parts[:-1]:
163 if p: # possible consecutive -
164 if p: # possible consecutive -
164 yield ('symbol', p, s)
165 yield ('symbol', p, s)
165 s += len(p)
166 s += len(p)
166 yield ('-', None, pos)
167 yield ('-', None, pos)
167 s += 1
168 s += 1
168 if parts[-1]: # possible trailing -
169 if parts[-1]: # possible trailing -
169 yield ('symbol', parts[-1], s)
170 yield ('symbol', parts[-1], s)
170 else:
171 else:
171 yield ('symbol', sym, s)
172 yield ('symbol', sym, s)
172 pos -= 1
173 pos -= 1
173 else:
174 else:
174 raise error.ParseError(_("syntax error in revset '%s'") %
175 raise error.ParseError(_("syntax error in revset '%s'") %
175 program, pos)
176 program, pos)
176 pos += 1
177 pos += 1
177 yield ('end', None, pos)
178 yield ('end', None, pos)
178
179
179 # helpers
180 # helpers
180
181
181 _notset = object()
182 _notset = object()
182
183
183 def getsymbol(x):
184 def getsymbol(x):
184 if x and x[0] == 'symbol':
185 if x and x[0] == 'symbol':
185 return x[1]
186 return x[1]
186 raise error.ParseError(_('not a symbol'))
187 raise error.ParseError(_('not a symbol'))
187
188
188 def getstring(x, err):
189 def getstring(x, err):
189 if x and (x[0] == 'string' or x[0] == 'symbol'):
190 if x and (x[0] == 'string' or x[0] == 'symbol'):
190 return x[1]
191 return x[1]
191 raise error.ParseError(err)
192 raise error.ParseError(err)
192
193
193 def getinteger(x, err, default=_notset):
194 def getinteger(x, err, default=_notset):
194 if not x and default is not _notset:
195 if not x and default is not _notset:
195 return default
196 return default
196 try:
197 try:
197 return int(getstring(x, err))
198 return int(getstring(x, err))
198 except ValueError:
199 except ValueError:
199 raise error.ParseError(err)
200 raise error.ParseError(err)
200
201
201 def getlist(x):
202 def getlist(x):
202 if not x:
203 if not x:
203 return []
204 return []
204 if x[0] == 'list':
205 if x[0] == 'list':
205 return list(x[1:])
206 return list(x[1:])
206 return [x]
207 return [x]
207
208
208 def getrange(x, err):
209 def getrange(x, err):
209 if not x:
210 if not x:
210 raise error.ParseError(err)
211 raise error.ParseError(err)
211 op = x[0]
212 op = x[0]
212 if op == 'range':
213 if op == 'range':
213 return x[1], x[2]
214 return x[1], x[2]
214 elif op == 'rangepre':
215 elif op == 'rangepre':
215 return None, x[1]
216 return None, x[1]
216 elif op == 'rangepost':
217 elif op == 'rangepost':
217 return x[1], None
218 return x[1], None
218 elif op == 'rangeall':
219 elif op == 'rangeall':
219 return None, None
220 return None, None
220 raise error.ParseError(err)
221 raise error.ParseError(err)
221
222
222 def getargs(x, min, max, err):
223 def getargs(x, min, max, err):
223 l = getlist(x)
224 l = getlist(x)
224 if len(l) < min or (max >= 0 and len(l) > max):
225 if len(l) < min or (max >= 0 and len(l) > max):
225 raise error.ParseError(err)
226 raise error.ParseError(err)
226 return l
227 return l
227
228
228 def getargsdict(x, funcname, keys):
229 def getargsdict(x, funcname, keys):
229 return parser.buildargsdict(getlist(x), funcname, parser.splitargspec(keys),
230 return parser.buildargsdict(getlist(x), funcname, parser.splitargspec(keys),
230 keyvaluenode='keyvalue', keynode='symbol')
231 keyvaluenode='keyvalue', keynode='symbol')
231
232
232 # Constants for ordering requirement, used in _analyze():
233 # Constants for ordering requirement, used in _analyze():
233 #
234 #
234 # If 'define', any nested functions and operations can change the ordering of
235 # If 'define', any nested functions and operations can change the ordering of
235 # the entries in the set. If 'follow', any nested functions and operations
236 # the entries in the set. If 'follow', any nested functions and operations
236 # should take the ordering specified by the first operand to the '&' operator.
237 # should take the ordering specified by the first operand to the '&' operator.
237 #
238 #
238 # For instance,
239 # For instance,
239 #
240 #
240 # X & (Y | Z)
241 # X & (Y | Z)
241 # ^ ^^^^^^^
242 # ^ ^^^^^^^
242 # | follow
243 # | follow
243 # define
244 # define
244 #
245 #
245 # will be evaluated as 'or(y(x()), z(x()))', where 'x()' can change the order
246 # will be evaluated as 'or(y(x()), z(x()))', where 'x()' can change the order
246 # of the entries in the set, but 'y()', 'z()' and 'or()' shouldn't.
247 # of the entries in the set, but 'y()', 'z()' and 'or()' shouldn't.
247 #
248 #
248 # 'any' means the order doesn't matter. For instance,
249 # 'any' means the order doesn't matter. For instance,
249 #
250 #
250 # X & !Y
251 # X & !Y
251 # ^
252 # ^
252 # any
253 # any
253 #
254 #
254 # 'y()' can either enforce its ordering requirement or take the ordering
255 # 'y()' can either enforce its ordering requirement or take the ordering
255 # specified by 'x()' because 'not()' doesn't care the order.
256 # specified by 'x()' because 'not()' doesn't care the order.
256 #
257 #
257 # Transition of ordering requirement:
258 # Transition of ordering requirement:
258 #
259 #
259 # 1. starts with 'define'
260 # 1. starts with 'define'
260 # 2. shifts to 'follow' by 'x & y'
261 # 2. shifts to 'follow' by 'x & y'
261 # 3. changes back to 'define' on function call 'f(x)' or function-like
262 # 3. changes back to 'define' on function call 'f(x)' or function-like
262 # operation 'x (f) y' because 'f' may have its own ordering requirement
263 # operation 'x (f) y' because 'f' may have its own ordering requirement
263 # for 'x' and 'y' (e.g. 'first(x)')
264 # for 'x' and 'y' (e.g. 'first(x)')
264 #
265 #
265 anyorder = 'any' # don't care the order
266 anyorder = 'any' # don't care the order
266 defineorder = 'define' # should define the order
267 defineorder = 'define' # should define the order
267 followorder = 'follow' # must follow the current order
268 followorder = 'follow' # must follow the current order
268
269
269 # transition table for 'x & y', from the current expression 'x' to 'y'
270 # transition table for 'x & y', from the current expression 'x' to 'y'
270 _tofolloworder = {
271 _tofolloworder = {
271 anyorder: anyorder,
272 anyorder: anyorder,
272 defineorder: followorder,
273 defineorder: followorder,
273 followorder: followorder,
274 followorder: followorder,
274 }
275 }
275
276
276 def _matchonly(revs, bases):
277 def _matchonly(revs, bases):
277 """
278 """
278 >>> f = lambda *args: _matchonly(*map(parse, args))
279 >>> f = lambda *args: _matchonly(*map(parse, args))
279 >>> f('ancestors(A)', 'not ancestors(B)')
280 >>> f('ancestors(A)', 'not ancestors(B)')
280 ('list', ('symbol', 'A'), ('symbol', 'B'))
281 ('list', ('symbol', 'A'), ('symbol', 'B'))
281 """
282 """
282 if (revs is not None
283 if (revs is not None
283 and revs[0] == 'func'
284 and revs[0] == 'func'
284 and getsymbol(revs[1]) == 'ancestors'
285 and getsymbol(revs[1]) == 'ancestors'
285 and bases is not None
286 and bases is not None
286 and bases[0] == 'not'
287 and bases[0] == 'not'
287 and bases[1][0] == 'func'
288 and bases[1][0] == 'func'
288 and getsymbol(bases[1][1]) == 'ancestors'):
289 and getsymbol(bases[1][1]) == 'ancestors'):
289 return ('list', revs[2], bases[1][2])
290 return ('list', revs[2], bases[1][2])
290
291
291 def _fixops(x):
292 def _fixops(x):
292 """Rewrite raw parsed tree to resolve ambiguous syntax which cannot be
293 """Rewrite raw parsed tree to resolve ambiguous syntax which cannot be
293 handled well by our simple top-down parser"""
294 handled well by our simple top-down parser"""
294 if not isinstance(x, tuple):
295 if not isinstance(x, tuple):
295 return x
296 return x
296
297
297 op = x[0]
298 op = x[0]
298 if op == 'parent':
299 if op == 'parent':
299 # x^:y means (x^) : y, not x ^ (:y)
300 # x^:y means (x^) : y, not x ^ (:y)
300 # x^: means (x^) :, not x ^ (:)
301 # x^: means (x^) :, not x ^ (:)
301 post = ('parentpost', x[1])
302 post = ('parentpost', x[1])
302 if x[2][0] == 'dagrangepre':
303 if x[2][0] == 'dagrangepre':
303 return _fixops(('dagrange', post, x[2][1]))
304 return _fixops(('dagrange', post, x[2][1]))
304 elif x[2][0] == 'rangepre':
305 elif x[2][0] == 'rangepre':
305 return _fixops(('range', post, x[2][1]))
306 return _fixops(('range', post, x[2][1]))
306 elif x[2][0] == 'rangeall':
307 elif x[2][0] == 'rangeall':
307 return _fixops(('rangepost', post))
308 return _fixops(('rangepost', post))
308 elif op == 'or':
309 elif op == 'or':
309 # make number of arguments deterministic:
310 # make number of arguments deterministic:
310 # x + y + z -> (or x y z) -> (or (list x y z))
311 # x + y + z -> (or x y z) -> (or (list x y z))
311 return (op, _fixops(('list',) + x[1:]))
312 return (op, _fixops(('list',) + x[1:]))
312
313
313 return (op,) + tuple(_fixops(y) for y in x[1:])
314 return (op,) + tuple(_fixops(y) for y in x[1:])
314
315
315 def _analyze(x, order):
316 def _analyze(x, order):
316 if x is None:
317 if x is None:
317 return x
318 return x
318
319
319 op = x[0]
320 op = x[0]
320 if op == 'minus':
321 if op == 'minus':
321 return _analyze(('and', x[1], ('not', x[2])), order)
322 return _analyze(('and', x[1], ('not', x[2])), order)
322 elif op == 'only':
323 elif op == 'only':
323 t = ('func', ('symbol', 'only'), ('list', x[1], x[2]))
324 t = ('func', ('symbol', 'only'), ('list', x[1], x[2]))
324 return _analyze(t, order)
325 return _analyze(t, order)
325 elif op == 'onlypost':
326 elif op == 'onlypost':
326 return _analyze(('func', ('symbol', 'only'), x[1]), order)
327 return _analyze(('func', ('symbol', 'only'), x[1]), order)
327 elif op == 'dagrangepre':
328 elif op == 'dagrangepre':
328 return _analyze(('func', ('symbol', 'ancestors'), x[1]), order)
329 return _analyze(('func', ('symbol', 'ancestors'), x[1]), order)
329 elif op == 'dagrangepost':
330 elif op == 'dagrangepost':
330 return _analyze(('func', ('symbol', 'descendants'), x[1]), order)
331 return _analyze(('func', ('symbol', 'descendants'), x[1]), order)
331 elif op == 'negate':
332 elif op == 'negate':
332 s = getstring(x[1], _("can't negate that"))
333 s = getstring(x[1], _("can't negate that"))
333 return _analyze(('string', '-' + s), order)
334 return _analyze(('string', '-' + s), order)
334 elif op in ('string', 'symbol'):
335 elif op in ('string', 'symbol'):
335 return x
336 return x
336 elif op == 'and':
337 elif op == 'and':
337 ta = _analyze(x[1], order)
338 ta = _analyze(x[1], order)
338 tb = _analyze(x[2], _tofolloworder[order])
339 tb = _analyze(x[2], _tofolloworder[order])
339 return (op, ta, tb, order)
340 return (op, ta, tb, order)
340 elif op == 'or':
341 elif op == 'or':
341 return (op, _analyze(x[1], order), order)
342 return (op, _analyze(x[1], order), order)
342 elif op == 'not':
343 elif op == 'not':
343 return (op, _analyze(x[1], anyorder), order)
344 return (op, _analyze(x[1], anyorder), order)
344 elif op == 'rangeall':
345 elif op == 'rangeall':
345 return (op, None, order)
346 return (op, None, order)
346 elif op in ('rangepre', 'rangepost', 'parentpost'):
347 elif op in ('rangepre', 'rangepost', 'parentpost'):
347 return (op, _analyze(x[1], defineorder), order)
348 return (op, _analyze(x[1], defineorder), order)
348 elif op == 'group':
349 elif op == 'group':
349 return _analyze(x[1], order)
350 return _analyze(x[1], order)
350 elif op in ('dagrange', 'range', 'parent', 'ancestor'):
351 elif op in ('dagrange', 'range', 'parent', 'ancestor'):
351 ta = _analyze(x[1], defineorder)
352 ta = _analyze(x[1], defineorder)
352 tb = _analyze(x[2], defineorder)
353 tb = _analyze(x[2], defineorder)
353 return (op, ta, tb, order)
354 return (op, ta, tb, order)
354 elif op == 'list':
355 elif op == 'list':
355 return (op,) + tuple(_analyze(y, order) for y in x[1:])
356 return (op,) + tuple(_analyze(y, order) for y in x[1:])
356 elif op == 'keyvalue':
357 elif op == 'keyvalue':
357 return (op, x[1], _analyze(x[2], order))
358 return (op, x[1], _analyze(x[2], order))
358 elif op == 'func':
359 elif op == 'func':
359 f = getsymbol(x[1])
360 f = getsymbol(x[1])
360 d = defineorder
361 d = defineorder
361 if f == 'present':
362 if f == 'present':
362 # 'present(set)' is known to return the argument set with no
363 # 'present(set)' is known to return the argument set with no
363 # modification, so forward the current order to its argument
364 # modification, so forward the current order to its argument
364 d = order
365 d = order
365 return (op, x[1], _analyze(x[2], d), order)
366 return (op, x[1], _analyze(x[2], d), order)
366 raise ValueError('invalid operator %r' % op)
367 raise ValueError('invalid operator %r' % op)
367
368
368 def analyze(x, order=defineorder):
369 def analyze(x, order=defineorder):
369 """Transform raw parsed tree to evaluatable tree which can be fed to
370 """Transform raw parsed tree to evaluatable tree which can be fed to
370 optimize() or getset()
371 optimize() or getset()
371
372
372 All pseudo operations should be mapped to real operations or functions
373 All pseudo operations should be mapped to real operations or functions
373 defined in methods or symbols table respectively.
374 defined in methods or symbols table respectively.
374
375
375 'order' specifies how the current expression 'x' is ordered (see the
376 'order' specifies how the current expression 'x' is ordered (see the
376 constants defined above.)
377 constants defined above.)
377 """
378 """
378 return _analyze(x, order)
379 return _analyze(x, order)
379
380
380 def _optimize(x, small):
381 def _optimize(x, small):
381 if x is None:
382 if x is None:
382 return 0, x
383 return 0, x
383
384
384 smallbonus = 1
385 smallbonus = 1
385 if small:
386 if small:
386 smallbonus = .5
387 smallbonus = .5
387
388
388 op = x[0]
389 op = x[0]
389 if op in ('string', 'symbol'):
390 if op in ('string', 'symbol'):
390 return smallbonus, x # single revisions are small
391 return smallbonus, x # single revisions are small
391 elif op == 'and':
392 elif op == 'and':
392 wa, ta = _optimize(x[1], True)
393 wa, ta = _optimize(x[1], True)
393 wb, tb = _optimize(x[2], True)
394 wb, tb = _optimize(x[2], True)
394 order = x[3]
395 order = x[3]
395 w = min(wa, wb)
396 w = min(wa, wb)
396
397
397 # (::x and not ::y)/(not ::y and ::x) have a fast path
398 # (::x and not ::y)/(not ::y and ::x) have a fast path
398 tm = _matchonly(ta, tb) or _matchonly(tb, ta)
399 tm = _matchonly(ta, tb) or _matchonly(tb, ta)
399 if tm:
400 if tm:
400 return w, ('func', ('symbol', 'only'), tm, order)
401 return w, ('func', ('symbol', 'only'), tm, order)
401
402
402 if tb is not None and tb[0] == 'not':
403 if tb is not None and tb[0] == 'not':
403 return wa, ('difference', ta, tb[1], order)
404 return wa, ('difference', ta, tb[1], order)
404
405
405 if wa > wb:
406 if wa > wb:
406 return w, (op, tb, ta, order)
407 return w, (op, tb, ta, order)
407 return w, (op, ta, tb, order)
408 return w, (op, ta, tb, order)
408 elif op == 'or':
409 elif op == 'or':
409 # fast path for machine-generated expression, that is likely to have
410 # fast path for machine-generated expression, that is likely to have
410 # lots of trivial revisions: 'a + b + c()' to '_list(a b) + c()'
411 # lots of trivial revisions: 'a + b + c()' to '_list(a b) + c()'
411 order = x[2]
412 order = x[2]
412 ws, ts, ss = [], [], []
413 ws, ts, ss = [], [], []
413 def flushss():
414 def flushss():
414 if not ss:
415 if not ss:
415 return
416 return
416 if len(ss) == 1:
417 if len(ss) == 1:
417 w, t = ss[0]
418 w, t = ss[0]
418 else:
419 else:
419 s = '\0'.join(t[1] for w, t in ss)
420 s = '\0'.join(t[1] for w, t in ss)
420 y = ('func', ('symbol', '_list'), ('string', s), order)
421 y = ('func', ('symbol', '_list'), ('string', s), order)
421 w, t = _optimize(y, False)
422 w, t = _optimize(y, False)
422 ws.append(w)
423 ws.append(w)
423 ts.append(t)
424 ts.append(t)
424 del ss[:]
425 del ss[:]
425 for y in getlist(x[1]):
426 for y in getlist(x[1]):
426 w, t = _optimize(y, False)
427 w, t = _optimize(y, False)
427 if t is not None and (t[0] == 'string' or t[0] == 'symbol'):
428 if t is not None and (t[0] == 'string' or t[0] == 'symbol'):
428 ss.append((w, t))
429 ss.append((w, t))
429 continue
430 continue
430 flushss()
431 flushss()
431 ws.append(w)
432 ws.append(w)
432 ts.append(t)
433 ts.append(t)
433 flushss()
434 flushss()
434 if len(ts) == 1:
435 if len(ts) == 1:
435 return ws[0], ts[0] # 'or' operation is fully optimized out
436 return ws[0], ts[0] # 'or' operation is fully optimized out
436 # we can't reorder trees by weight because it would change the order.
437 # we can't reorder trees by weight because it would change the order.
437 # ("sort(a + b)" == "sort(b + a)", but "a + b" != "b + a")
438 # ("sort(a + b)" == "sort(b + a)", but "a + b" != "b + a")
438 # ts = tuple(t for w, t in sorted(zip(ws, ts), key=lambda wt: wt[0]))
439 # ts = tuple(t for w, t in sorted(zip(ws, ts), key=lambda wt: wt[0]))
439 return max(ws), (op, ('list',) + tuple(ts), order)
440 return max(ws), (op, ('list',) + tuple(ts), order)
440 elif op == 'not':
441 elif op == 'not':
441 # Optimize not public() to _notpublic() because we have a fast version
442 # Optimize not public() to _notpublic() because we have a fast version
442 if x[1][:3] == ('func', ('symbol', 'public'), None):
443 if x[1][:3] == ('func', ('symbol', 'public'), None):
443 order = x[1][3]
444 order = x[1][3]
444 newsym = ('func', ('symbol', '_notpublic'), None, order)
445 newsym = ('func', ('symbol', '_notpublic'), None, order)
445 o = _optimize(newsym, not small)
446 o = _optimize(newsym, not small)
446 return o[0], o[1]
447 return o[0], o[1]
447 else:
448 else:
448 o = _optimize(x[1], not small)
449 o = _optimize(x[1], not small)
449 order = x[2]
450 order = x[2]
450 return o[0], (op, o[1], order)
451 return o[0], (op, o[1], order)
451 elif op == 'rangeall':
452 elif op == 'rangeall':
452 return smallbonus, x
453 return smallbonus, x
453 elif op in ('rangepre', 'rangepost', 'parentpost'):
454 elif op in ('rangepre', 'rangepost', 'parentpost'):
454 o = _optimize(x[1], small)
455 o = _optimize(x[1], small)
455 order = x[2]
456 order = x[2]
456 return o[0], (op, o[1], order)
457 return o[0], (op, o[1], order)
457 elif op in ('dagrange', 'range', 'parent', 'ancestor'):
458 elif op in ('dagrange', 'range', 'parent', 'ancestor'):
458 wa, ta = _optimize(x[1], small)
459 wa, ta = _optimize(x[1], small)
459 wb, tb = _optimize(x[2], small)
460 wb, tb = _optimize(x[2], small)
460 order = x[3]
461 order = x[3]
461 return wa + wb, (op, ta, tb, order)
462 return wa + wb, (op, ta, tb, order)
462 elif op == 'list':
463 elif op == 'list':
463 ws, ts = zip(*(_optimize(y, small) for y in x[1:]))
464 ws, ts = zip(*(_optimize(y, small) for y in x[1:]))
464 return sum(ws), (op,) + ts
465 return sum(ws), (op,) + ts
465 elif op == 'keyvalue':
466 elif op == 'keyvalue':
466 w, t = _optimize(x[2], small)
467 w, t = _optimize(x[2], small)
467 return w, (op, x[1], t)
468 return w, (op, x[1], t)
468 elif op == 'func':
469 elif op == 'func':
469 f = getsymbol(x[1])
470 f = getsymbol(x[1])
470 wa, ta = _optimize(x[2], small)
471 wa, ta = _optimize(x[2], small)
471 if f in ('author', 'branch', 'closed', 'date', 'desc', 'file', 'grep',
472 if f in ('author', 'branch', 'closed', 'date', 'desc', 'file', 'grep',
472 'keyword', 'outgoing', 'user', 'destination'):
473 'keyword', 'outgoing', 'user', 'destination'):
473 w = 10 # slow
474 w = 10 # slow
474 elif f in ('modifies', 'adds', 'removes'):
475 elif f in ('modifies', 'adds', 'removes'):
475 w = 30 # slower
476 w = 30 # slower
476 elif f == "contains":
477 elif f == "contains":
477 w = 100 # very slow
478 w = 100 # very slow
478 elif f == "ancestor":
479 elif f == "ancestor":
479 w = 1 * smallbonus
480 w = 1 * smallbonus
480 elif f in ('reverse', 'limit', 'first', 'wdir', '_intlist'):
481 elif f in ('reverse', 'limit', 'first', 'wdir', '_intlist'):
481 w = 0
482 w = 0
482 elif f == "sort":
483 elif f == "sort":
483 w = 10 # assume most sorts look at changelog
484 w = 10 # assume most sorts look at changelog
484 else:
485 else:
485 w = 1
486 w = 1
486 order = x[3]
487 order = x[3]
487 return w + wa, (op, x[1], ta, order)
488 return w + wa, (op, x[1], ta, order)
488 raise ValueError('invalid operator %r' % op)
489 raise ValueError('invalid operator %r' % op)
489
490
490 def optimize(tree):
491 def optimize(tree):
491 """Optimize evaluatable tree
492 """Optimize evaluatable tree
492
493
493 All pseudo operations should be transformed beforehand.
494 All pseudo operations should be transformed beforehand.
494 """
495 """
495 _weight, newtree = _optimize(tree, small=True)
496 _weight, newtree = _optimize(tree, small=True)
496 return newtree
497 return newtree
497
498
498 # the set of valid characters for the initial letter of symbols in
499 # the set of valid characters for the initial letter of symbols in
499 # alias declarations and definitions
500 # alias declarations and definitions
500 _aliassyminitletters = _syminitletters | set(pycompat.sysstr('$'))
501 _aliassyminitletters = _syminitletters | set(pycompat.sysstr('$'))
501
502
502 def _parsewith(spec, lookup=None, syminitletters=None):
503 def _parsewith(spec, lookup=None, syminitletters=None):
503 """Generate a parse tree of given spec with given tokenizing options
504 """Generate a parse tree of given spec with given tokenizing options
504
505
505 >>> _parsewith('foo($1)', syminitletters=_aliassyminitletters)
506 >>> _parsewith('foo($1)', syminitletters=_aliassyminitletters)
506 ('func', ('symbol', 'foo'), ('symbol', '$1'))
507 ('func', ('symbol', 'foo'), ('symbol', '$1'))
507 >>> _parsewith('$1')
508 >>> _parsewith('$1')
508 Traceback (most recent call last):
509 Traceback (most recent call last):
509 ...
510 ...
510 ParseError: ("syntax error in revset '$1'", 0)
511 ParseError: ("syntax error in revset '$1'", 0)
511 >>> _parsewith('foo bar')
512 >>> _parsewith('foo bar')
512 Traceback (most recent call last):
513 Traceback (most recent call last):
513 ...
514 ...
514 ParseError: ('invalid token', 4)
515 ParseError: ('invalid token', 4)
515 """
516 """
516 p = parser.parser(elements)
517 p = parser.parser(elements)
517 tree, pos = p.parse(tokenize(spec, lookup=lookup,
518 tree, pos = p.parse(tokenize(spec, lookup=lookup,
518 syminitletters=syminitletters))
519 syminitletters=syminitletters))
519 if pos != len(spec):
520 if pos != len(spec):
520 raise error.ParseError(_('invalid token'), pos)
521 raise error.ParseError(_('invalid token'), pos)
521 return _fixops(parser.simplifyinfixops(tree, ('list', 'or')))
522 return _fixops(parser.simplifyinfixops(tree, ('list', 'or')))
522
523
523 class _aliasrules(parser.basealiasrules):
524 class _aliasrules(parser.basealiasrules):
524 """Parsing and expansion rule set of revset aliases"""
525 """Parsing and expansion rule set of revset aliases"""
525 _section = _('revset alias')
526 _section = _('revset alias')
526
527
527 @staticmethod
528 @staticmethod
528 def _parse(spec):
529 def _parse(spec):
529 """Parse alias declaration/definition ``spec``
530 """Parse alias declaration/definition ``spec``
530
531
531 This allows symbol names to use also ``$`` as an initial letter
532 This allows symbol names to use also ``$`` as an initial letter
532 (for backward compatibility), and callers of this function should
533 (for backward compatibility), and callers of this function should
533 examine whether ``$`` is used also for unexpected symbols or not.
534 examine whether ``$`` is used also for unexpected symbols or not.
534 """
535 """
535 return _parsewith(spec, syminitletters=_aliassyminitletters)
536 return _parsewith(spec, syminitletters=_aliassyminitletters)
536
537
537 @staticmethod
538 @staticmethod
538 def _trygetfunc(tree):
539 def _trygetfunc(tree):
539 if tree[0] == 'func' and tree[1][0] == 'symbol':
540 if tree[0] == 'func' and tree[1][0] == 'symbol':
540 return tree[1][1], getlist(tree[2])
541 return tree[1][1], getlist(tree[2])
541
542
542 def expandaliases(ui, tree):
543 def expandaliases(ui, tree):
543 aliases = _aliasrules.buildmap(ui.configitems('revsetalias'))
544 aliases = _aliasrules.buildmap(ui.configitems('revsetalias'))
544 tree = _aliasrules.expand(aliases, tree)
545 tree = _aliasrules.expand(aliases, tree)
545 # warn about problematic (but not referred) aliases
546 # warn about problematic (but not referred) aliases
546 for name, alias in sorted(aliases.iteritems()):
547 for name, alias in sorted(aliases.iteritems()):
547 if alias.error and not alias.warned:
548 if alias.error and not alias.warned:
548 ui.warn(_('warning: %s\n') % (alias.error))
549 ui.warn(_('warning: %s\n') % (alias.error))
549 alias.warned = True
550 alias.warned = True
550 return tree
551 return tree
551
552
552 def foldconcat(tree):
553 def foldconcat(tree):
553 """Fold elements to be concatenated by `##`
554 """Fold elements to be concatenated by `##`
554 """
555 """
555 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
556 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
556 return tree
557 return tree
557 if tree[0] == '_concat':
558 if tree[0] == '_concat':
558 pending = [tree]
559 pending = [tree]
559 l = []
560 l = []
560 while pending:
561 while pending:
561 e = pending.pop()
562 e = pending.pop()
562 if e[0] == '_concat':
563 if e[0] == '_concat':
563 pending.extend(reversed(e[1:]))
564 pending.extend(reversed(e[1:]))
564 elif e[0] in ('string', 'symbol'):
565 elif e[0] in ('string', 'symbol'):
565 l.append(e[1])
566 l.append(e[1])
566 else:
567 else:
567 msg = _("\"##\" can't concatenate \"%s\" element") % (e[0])
568 msg = _("\"##\" can't concatenate \"%s\" element") % (e[0])
568 raise error.ParseError(msg)
569 raise error.ParseError(msg)
569 return ('string', ''.join(l))
570 return ('string', ''.join(l))
570 else:
571 else:
571 return tuple(foldconcat(t) for t in tree)
572 return tuple(foldconcat(t) for t in tree)
572
573
573 def parse(spec, lookup=None):
574 def parse(spec, lookup=None):
574 return _parsewith(spec, lookup=lookup)
575 return _parsewith(spec, lookup=lookup)
575
576
576 def _quote(s):
577 def _quote(s):
577 r"""Quote a value in order to make it safe for the revset engine.
578 r"""Quote a value in order to make it safe for the revset engine.
578
579
579 >>> _quote('asdf')
580 >>> _quote('asdf')
580 "'asdf'"
581 "'asdf'"
581 >>> _quote("asdf'\"")
582 >>> _quote("asdf'\"")
582 '\'asdf\\\'"\''
583 '\'asdf\\\'"\''
583 >>> _quote('asdf\'')
584 >>> _quote('asdf\'')
584 '"asdf\'"'
585 "'asdf\\''"
585 >>> _quote(1)
586 >>> _quote(1)
586 "'1'"
587 "'1'"
587 """
588 """
588 return repr(str(s))
589 return "'%s'" % util.escapestr('%s' % s)
589
590
590 def formatspec(expr, *args):
591 def formatspec(expr, *args):
591 '''
592 '''
592 This is a convenience function for using revsets internally, and
593 This is a convenience function for using revsets internally, and
593 escapes arguments appropriately. Aliases are intentionally ignored
594 escapes arguments appropriately. Aliases are intentionally ignored
594 so that intended expression behavior isn't accidentally subverted.
595 so that intended expression behavior isn't accidentally subverted.
595
596
596 Supported arguments:
597 Supported arguments:
597
598
598 %r = revset expression, parenthesized
599 %r = revset expression, parenthesized
599 %d = int(arg), no quoting
600 %d = int(arg), no quoting
600 %s = string(arg), escaped and single-quoted
601 %s = string(arg), escaped and single-quoted
601 %b = arg.branch(), escaped and single-quoted
602 %b = arg.branch(), escaped and single-quoted
602 %n = hex(arg), single-quoted
603 %n = hex(arg), single-quoted
603 %% = a literal '%'
604 %% = a literal '%'
604
605
605 Prefixing the type with 'l' specifies a parenthesized list of that type.
606 Prefixing the type with 'l' specifies a parenthesized list of that type.
606
607
607 >>> formatspec('%r:: and %lr', '10 or 11', ("this()", "that()"))
608 >>> formatspec('%r:: and %lr', '10 or 11', ("this()", "that()"))
608 '(10 or 11):: and ((this()) or (that()))'
609 '(10 or 11):: and ((this()) or (that()))'
609 >>> formatspec('%d:: and not %d::', 10, 20)
610 >>> formatspec('%d:: and not %d::', 10, 20)
610 '10:: and not 20::'
611 '10:: and not 20::'
611 >>> formatspec('%ld or %ld', [], [1])
612 >>> formatspec('%ld or %ld', [], [1])
612 "_list('') or 1"
613 "_list('') or 1"
613 >>> formatspec('keyword(%s)', 'foo\\xe9')
614 >>> formatspec('keyword(%s)', 'foo\\xe9')
614 "keyword('foo\\\\xe9')"
615 "keyword('foo\\\\xe9')"
615 >>> b = lambda: 'default'
616 >>> b = lambda: 'default'
616 >>> b.branch = b
617 >>> b.branch = b
617 >>> formatspec('branch(%b)', b)
618 >>> formatspec('branch(%b)', b)
618 "branch('default')"
619 "branch('default')"
619 >>> formatspec('root(%ls)', ['a', 'b', 'c', 'd'])
620 >>> formatspec('root(%ls)', ['a', 'b', 'c', 'd'])
620 "root(_list('a\\x00b\\x00c\\x00d'))"
621 "root(_list('a\\x00b\\x00c\\x00d'))"
621 '''
622 '''
622
623
623 def argtype(c, arg):
624 def argtype(c, arg):
624 if c == 'd':
625 if c == 'd':
625 return '%d' % int(arg)
626 return '%d' % int(arg)
626 elif c == 's':
627 elif c == 's':
627 return _quote(arg)
628 return _quote(arg)
628 elif c == 'r':
629 elif c == 'r':
629 parse(arg) # make sure syntax errors are confined
630 parse(arg) # make sure syntax errors are confined
630 return '(%s)' % arg
631 return '(%s)' % arg
631 elif c == 'n':
632 elif c == 'n':
632 return _quote(node.hex(arg))
633 return _quote(node.hex(arg))
633 elif c == 'b':
634 elif c == 'b':
634 return _quote(arg.branch())
635 return _quote(arg.branch())
635
636
636 def listexp(s, t):
637 def listexp(s, t):
637 l = len(s)
638 l = len(s)
638 if l == 0:
639 if l == 0:
639 return "_list('')"
640 return "_list('')"
640 elif l == 1:
641 elif l == 1:
641 return argtype(t, s[0])
642 return argtype(t, s[0])
642 elif t == 'd':
643 elif t == 'd':
643 return "_intlist('%s')" % "\0".join('%d' % int(a) for a in s)
644 return "_intlist('%s')" % "\0".join('%d' % int(a) for a in s)
644 elif t == 's':
645 elif t == 's':
645 return "_list('%s')" % "\0".join(s)
646 return "_list('%s')" % "\0".join(s)
646 elif t == 'n':
647 elif t == 'n':
647 return "_hexlist('%s')" % "\0".join(node.hex(a) for a in s)
648 return "_hexlist('%s')" % "\0".join(node.hex(a) for a in s)
648 elif t == 'b':
649 elif t == 'b':
649 return "_list('%s')" % "\0".join(a.branch() for a in s)
650 return "_list('%s')" % "\0".join(a.branch() for a in s)
650
651
651 m = l // 2
652 m = l // 2
652 return '(%s or %s)' % (listexp(s[:m], t), listexp(s[m:], t))
653 return '(%s or %s)' % (listexp(s[:m], t), listexp(s[m:], t))
653
654
654 expr = pycompat.bytestr(expr)
655 expr = pycompat.bytestr(expr)
655 ret = ''
656 ret = ''
656 pos = 0
657 pos = 0
657 arg = 0
658 arg = 0
658 while pos < len(expr):
659 while pos < len(expr):
659 c = expr[pos]
660 c = expr[pos]
660 if c == '%':
661 if c == '%':
661 pos += 1
662 pos += 1
662 d = expr[pos]
663 d = expr[pos]
663 if d == '%':
664 if d == '%':
664 ret += d
665 ret += d
665 elif d in 'dsnbr':
666 elif d in 'dsnbr':
666 ret += argtype(d, args[arg])
667 ret += argtype(d, args[arg])
667 arg += 1
668 arg += 1
668 elif d == 'l':
669 elif d == 'l':
669 # a list of some type
670 # a list of some type
670 pos += 1
671 pos += 1
671 d = expr[pos]
672 d = expr[pos]
672 ret += listexp(list(args[arg]), d)
673 ret += listexp(list(args[arg]), d)
673 arg += 1
674 arg += 1
674 else:
675 else:
675 raise error.Abort(_('unexpected revspec format character %s')
676 raise error.Abort(_('unexpected revspec format character %s')
676 % d)
677 % d)
677 else:
678 else:
678 ret += c
679 ret += c
679 pos += 1
680 pos += 1
680
681
681 return ret
682 return ret
682
683
683 def prettyformat(tree):
684 def prettyformat(tree):
684 return parser.prettyformat(tree, ('string', 'symbol'))
685 return parser.prettyformat(tree, ('string', 'symbol'))
685
686
686 def depth(tree):
687 def depth(tree):
687 if isinstance(tree, tuple):
688 if isinstance(tree, tuple):
688 return max(map(depth, tree)) + 1
689 return max(map(depth, tree)) + 1
689 else:
690 else:
690 return 0
691 return 0
691
692
692 def funcsused(tree):
693 def funcsused(tree):
693 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
694 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
694 return set()
695 return set()
695 else:
696 else:
696 funcs = set()
697 funcs = set()
697 for s in tree[1:]:
698 for s in tree[1:]:
698 funcs |= funcsused(s)
699 funcs |= funcsused(s)
699 if tree[0] == 'func':
700 if tree[0] == 'func':
700 funcs.add(tree[1][1])
701 funcs.add(tree[1][1])
701 return funcs
702 return funcs
General Comments 0
You need to be logged in to leave comments. Login now