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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy