Skip to content

Commit 05c83be

Browse files
Avoid unsafe completion on array elements
This commit removes functionality it would be nice to reimplement but wasn't safe: * completion on nested arrays ( list[1][2].a ) * completion on subclasses of list, tuple etc.
1 parent 58f3edf commit 05c83be

File tree

4 files changed

+122
-26
lines changed

4 files changed

+122
-26
lines changed

bpython/autocomplete.py

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,10 @@ def matches(self, cursor_offset, line, **kwargs):
118118
raise NotImplementedError
119119

120120
def locate(self, cursor_offset, line):
121-
"""Returns a start, stop, and word given a line and cursor, or None
122-
if no target for this type of completion is found under the cursor"""
121+
"""Returns a Linepart namedtuple instance or None given cursor and line
122+
123+
A Linepart namedtuple contains a start, stop, and word. None is returned
124+
if no target for this type of completion is found under the cursor."""
123125
raise NotImplementedError
124126

125127
def format(self, word):
@@ -346,28 +348,47 @@ def matches(self, cursor_offset, line, **kwargs):
346348
return None
347349
locals_ = kwargs['locals_']
348350

349-
r = self.locate(cursor_offset, line)
350-
if r is None:
351+
full = self.locate(cursor_offset, line)
352+
if full is None:
351353
return None
352-
member_part = r[2]
353-
_, _, dexpr = lineparts.current_array_with_indexer(cursor_offset, line)
354+
355+
arr = lineparts.current_indexed_member_access_identifier(
356+
cursor_offset, line)
357+
index = lineparts.current_indexed_member_access_identifier_with_index(
358+
cursor_offset, line)
359+
member = lineparts.current_indexed_member_access_member(
360+
cursor_offset, line)
361+
354362
try:
355-
locals_['temp_val_from_array'] = safe_eval(dexpr, locals_)
363+
obj = safe_eval(arr.word, locals_)
364+
except EvaluationError:
365+
return None
366+
if type(obj) not in (list, tuple) + string_types:
367+
# then is may be unsafe to do attribute lookup on it
368+
return None
369+
370+
try:
371+
locals_['temp_val_from_array'] = safe_eval(index.word, locals_)
356372
except (EvaluationError, IndexError):
357-
return set()
373+
return None
358374

359-
temp_line = line.replace(member_part, 'temp_val_from_array.')
375+
temp_line = line.replace(index.word, 'temp_val_from_array.')
360376

361377
matches = self.completer.matches(len(temp_line), temp_line, **kwargs)
378+
if matches is None:
379+
return None
380+
362381
matches_with_correct_name = \
363-
set(match.replace('temp_val_from_array.', member_part) for match in matches)
382+
set(match.replace('temp_val_from_array.', index.word+'.')
383+
for match in matches if match[20:].startswith(member.word))
364384

365385
del locals_['temp_val_from_array']
366386

367387
return matches_with_correct_name
368388

369389
def locate(self, current_offset, line):
370-
return lineparts.current_array_item_member_name(current_offset, line)
390+
a = lineparts.current_indexed_member_access(current_offset, line)
391+
return a
371392

372393
def format(self, match):
373394
return after_last_dot(match)

bpython/line.py

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -228,25 +228,38 @@ def current_string_literal_attr(cursor_offset, line):
228228
return None
229229

230230

231-
current_array_with_indexer_re = LazyReCompile(
232-
r'''([\w_][\w0-9._]*(?:\[[a-zA-Z0-9_"']+\])+)\.(.*)''')
231+
current_indexed_member_re = LazyReCompile(
232+
r'''([a-zA-Z_][\w.]*)\[([a-zA-Z0-9_"']+)\]\.([\w.]*)''')
233233

234234

235-
def current_array_with_indexer(cursor_offset, line):
236-
"""an array and indexer, e.g. foo[1]"""
237-
matches = current_array_with_indexer_re.finditer(line)
235+
def current_indexed_member_access(cursor_offset, line):
236+
"""An identifier being indexed and member accessed"""
237+
matches = current_indexed_member_re.finditer(line)
238238
for m in matches:
239-
if m.start(1) <= cursor_offset and m.end(1) <= cursor_offset:
239+
if m.start(3) <= cursor_offset and m.end(3) >= cursor_offset:
240+
return LinePart(m.start(1), m.end(3), m.group())
241+
242+
243+
def current_indexed_member_access_identifier(cursor_offset, line):
244+
"""An identifier being indexed, e.g. foo in foo[1].bar"""
245+
matches = current_indexed_member_re.finditer(line)
246+
for m in matches:
247+
if m.start(3) <= cursor_offset and m.end(3) >= cursor_offset:
240248
return LinePart(m.start(1), m.end(1), m.group(1))
241249

242250

243-
current_array_item_member_name_re = LazyReCompile(
244-
r'''([\w_][\w0-9._]*(?:\[[a-zA-Z0-9_"']+\])+\.)(.*)''')
251+
def current_indexed_member_access_identifier_with_index(cursor_offset, line):
252+
"""An identifier being indexed with the index, e.g. foo[1] in foo[1].bar"""
253+
matches = current_indexed_member_re.finditer(line)
254+
for m in matches:
255+
if m.start(3) <= cursor_offset and m.end(3) >= cursor_offset:
256+
return LinePart(m.start(1), m.end(2)+1,
257+
"%s[%s]" % (m.group(1), m.group(2)))
245258

246259

247-
def current_array_item_member_name(cursor_offset, line):
248-
"""the member name after an array indexer, e.g. foo[1].bar"""
249-
matches = current_array_item_member_name_re.finditer(line)
260+
def current_indexed_member_access_member(cursor_offset, line):
261+
"""The member name of an indexed object, e.g. bar in foo[1].bar"""
262+
matches = current_indexed_member_re.finditer(line)
250263
for m in matches:
251-
if m.start(2) <= cursor_offset and m.end(2) >= cursor_offset:
252-
return LinePart(m.start(1), m.end(2), m.group(1))
264+
if m.start(3) <= cursor_offset and m.end(3) >= cursor_offset:
265+
return LinePart(m.start(3), m.end(3), m.group(3))

bpython/test/test_autocomplete.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,32 @@ def test_att_matches_found_on_old_style_instance(self):
283283
locals_={'a': [OldStyleFoo()]}),
284284
set(['a[0].method', 'a[0].a', 'a[0].b']))
285285

286+
def test_other_getitem_methods_not_called(self):
287+
class FakeList(object):
288+
def __getitem__(inner_self, i):
289+
self.fail("possibly side-effecting __getitem_ method called")
290+
291+
self.com.matches(5, 'a[0].', locals_={'a': FakeList()})
292+
293+
def test_tuples_complete(self):
294+
self.assertSetEqual(self.com.matches(5, 'a[0].',
295+
locals_={'a': (Foo(),)}),
296+
set(['a[0].method', 'a[0].a', 'a[0].b']))
297+
298+
@unittest.skip('TODO, subclasses do not complete yet')
299+
def test_list_subclasses_complete(self):
300+
class ListSubclass(list): pass
301+
self.assertSetEqual(self.com.matches(5, 'a[0].',
302+
locals_={'a': ListSubclass([Foo()])}),
303+
set(['a[0].method', 'a[0].a', 'a[0].b']))
304+
305+
def test_getitem_not_called_in_list_subclasses_overriding_getitem(self):
306+
class FakeList(list):
307+
def __getitem__(inner_self, i):
308+
self.fail("possibly side-effecting __getitem_ method called")
309+
310+
self.com.matches(5, 'a[0].', locals_={'a': FakeList()})
311+
286312

287313
class TestMagicMethodCompletion(unittest.TestCase):
288314

bpython/test/test_line_properties.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
current_string, current_object, current_object_attribute, \
66
current_from_import_from, current_from_import_import, current_import, \
77
current_method_definition_name, current_single_word, \
8-
current_string_literal_attr
8+
current_string_literal_attr, current_indexed_member_access_identifier, \
9+
current_indexed_member_access_identifier_with_index, \
10+
current_indexed_member_access_member
911

1012

1113
def cursor(s):
@@ -20,7 +22,7 @@ def decode(s):
2022

2123
if not s.count('|') == 1:
2224
raise ValueError('match helper needs | to occur once')
23-
if s.count('<') != s.count('>') or not s.count('<') in (0, 1):
25+
if s.count('<') != s.count('>') or s.count('<') not in (0, 1):
2426
raise ValueError('match helper needs <, and > to occur just once')
2527
matches = list(re.finditer(r'[<>|]', s))
2628
assert len(matches) in [1, 3], [m.group() for m in matches]
@@ -305,6 +307,40 @@ def test_simple(self):
305307
self.assertAccess('"hey".asdf d|')
306308
self.assertAccess('"hey".<|>')
307309

310+
class TestCurrentIndexedMemberAccessIdentifier(LineTestCase):
311+
def setUp(self):
312+
self.func = current_indexed_member_access_identifier
313+
314+
def test_simple(self):
315+
self.assertAccess('<abc>[def].ghi|')
316+
self.assertAccess('<abc>[def].|ghi')
317+
self.assertAccess('<abc>[def].gh|i')
318+
self.assertAccess('abc[def].gh |i')
319+
self.assertAccess('abc[def]|')
320+
321+
322+
class TestCurrentIndexedMemberAccessIdentifierWithIndex(LineTestCase):
323+
def setUp(self):
324+
self.func = current_indexed_member_access_identifier_with_index
325+
326+
def test_simple(self):
327+
self.assertAccess('<abc[def]>.ghi|')
328+
self.assertAccess('<abc[def]>.|ghi')
329+
self.assertAccess('<abc[def]>.gh|i')
330+
self.assertAccess('abc[def].gh |i')
331+
self.assertAccess('abc[def]|')
332+
333+
334+
class TestCurrentIndexedMemberAccessMember(LineTestCase):
335+
def setUp(self):
336+
self.func = current_indexed_member_access_member
337+
338+
def test_simple(self):
339+
self.assertAccess('abc[def].<ghi|>')
340+
self.assertAccess('abc[def].<|ghi>')
341+
self.assertAccess('abc[def].<gh|i>')
342+
self.assertAccess('abc[def].gh |i')
343+
self.assertAccess('abc[def]|')
308344

309345
if __name__ == '__main__':
310346
unittest.main()

0 commit comments

Comments
 (0)