Skip to content

Commit d19e008

Browse files
committed
Add support for ends/startswith + tuple
Add support for startswith and endswith calls where the argument is a tuple of prefixes/suffixes
1 parent 91a6a63 commit d19e008

File tree

3 files changed

+59
-10
lines changed

3 files changed

+59
-10
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ in code, as well as overlapping execution traces.
4040
|Comparison | != (does not equal operator) | Full string | a != "foo" |
4141
|Comparison | in (part of operator) | String fragment | "foo" in a |
4242
|Comparison | in (match with collection) | List of full strings | a in ["foo","bar"] |
43-
|Call | string.startswith | String prefix | a.startswith("foo") |
44-
|Call | string.endswith | String suffix | a.endswith("foo") |
43+
|Call | string.startswith | String prefix, or tuple of string prefixes | a.startswith("foo") |
44+
|Call | string.endswith | String suffix, or tuple of string suffixes | a.endswith("foo") |
4545
|Call | string.find | String fragment | a.find("foo") |
4646
|Call | string.index | String fragment | a.index("foo") |

string_extractor/string_collector.py

+41-6
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,39 @@ def _isStringClass(self, c):
2828
else:
2929
return False
3030

31+
def _isTupleClass(self, c):
32+
className = str(type(c))
33+
return className in ["<class '_ast.Tuple'>", "<class 'ast.Tuple'>"]
34+
3135
def _getStringValue(self, c):
36+
"""Parses a single string value in the AST tree"""
3237
if str(type(c)) in ["<class '_ast.Str'>", "<class 'str'>"]:
3338
return c.s
3439
else:
3540
return c.value
3641

42+
def _getStringValuesAsList(self, c):
43+
"""Parses a single string value or tuple of strings in an AST
44+
tree as a list of strings."""
45+
def _get_single(c):
46+
classname = str(type(c))
47+
if classname in ["<class '_ast.Str'>", "<class 'str'>"]:
48+
return [c.s]
49+
elif classname in [ "<class '_ast.Constant'>", "<class 'ast.Constant'>" ]:
50+
return [c.value]
51+
else:
52+
return []
53+
54+
classname = str(type(c))
55+
if classname in ["<class '_ast.Str'>", "<class 'str'>",
56+
"<class '_ast.Constant'>", "<class 'ast.Constant'>" ]:
57+
return _get_single(c)
58+
elif classname in ["<class '_ast.Tuple'>", "<class 'ast.Tuple'>"]:
59+
result = []
60+
for element in c.elts:
61+
result.extend(_get_single(element))
62+
return result
63+
3764
def visit_Compare(self, node):
3865
opstype = str(type(node.ops[0]))
3966
if opstype in ["<class '_ast.Eq'>","<class '_ast.NotEq'>",
@@ -59,11 +86,11 @@ def visit_Compare(self, node):
5986
if self._isStringClass(element):
6087
self.fullStrings.add(self._getStringValue(element))
6188

62-
def visit_Call(self,node):
89+
def visit_Call(self, node):
6390
try:
6491
attr = node.func.attr
92+
args = node.args
6593
arg0 = node.args[0]
66-
string = node.args[0].s
6794
except AttributeError:
6895
# Call without attribute or first argument
6996
# is not a regular string
@@ -72,10 +99,18 @@ def visit_Call(self,node):
7299
# Call without arguments
73100
return
74101

75-
if attr == "startswith" and self._isStringClass(arg0):
76-
self.prefixes.add(self._getStringValue(arg0))
77-
elif attr == "endswith" and self._isStringClass(arg0):
78-
self.suffixes.add(self._getStringValue(arg0))
102+
if attr == "startswith":
103+
for arg in node.args:
104+
if self._isStringClass(arg):
105+
self.prefixes.add(self._getStringValue(arg))
106+
elif self._isTupleClass(arg):
107+
self.prefixes.update(self._getStringValuesAsList(arg))
108+
elif attr == "endswith":
109+
for arg in node.args:
110+
if self._isStringClass(arg):
111+
self.suffixes.add(self._getStringValue(arg))
112+
elif self._isTupleClass(arg):
113+
self.suffixes.update(self._getStringValuesAsList(arg))
79114
elif attr == "index" and self._isStringClass(arg0):
80115
self.fragments.add(self._getStringValue(arg0))
81116
elif attr == "find" and self._isStringClass(arg0):

tests/tests.py

+16-2
Original file line numberDiff line numberDiff line change
@@ -118,18 +118,32 @@ def test_comparison_in(self):
118118
assert(output[0][0] == "FRAGMENT")
119119
assert(output[0][1] == "foo")
120120

121-
def test_call_startswith(self):
121+
def test_call_startswith_single(self):
122122
output = self.extractor.getInterestingStrings('a.startswith("foo")')
123123
assert(len(output) == 1)
124124
assert(output[0][0] == "PREFIX")
125125
assert(output[0][1] == "foo")
126126

127-
def test_call_endswith(self):
127+
def test_call_startswith_tuple(self):
128+
output = self.extractor.getInterestingStrings('a.startswith( ("foo", "bar") )')
129+
assert(len(output) == 2)
130+
assert(output[0][0] == "PREFIX")
131+
assert(output[1][0] == "PREFIX")
132+
assert(sorted(list(map( lambda x : x[1], output ))) == ["bar", "foo"])
133+
134+
def test_call_endswith_single(self):
128135
output = self.extractor.getInterestingStrings('a.endswith("foo")')
129136
assert(len(output) == 1)
130137
assert(output[0][0] == "SUFFIX")
131138
assert(output[0][1] == "foo")
132139

140+
def test_call_endswith_tuple(self):
141+
output = self.extractor.getInterestingStrings('a.endswith( ("foo", "bar") )')
142+
assert(len(output) == 2)
143+
assert(output[0][0] == "SUFFIX")
144+
assert(output[1][0] == "SUFFIX")
145+
assert(sorted(list(map( lambda x : x[1], output ))) == ["bar", "foo"])
146+
133147
def test_call_index(self):
134148
output = self.extractor.getInterestingStrings('a.index("foo")')
135149
assert(len(output) == 1)

0 commit comments

Comments
 (0)