@@ -55,7 +55,7 @@ def test_to_ir(self):
5555 assert ir ["type" ] == "call_matcher"
5656 assert ir ["patterns" ] == ["eval" , "exec" ]
5757 assert ir ["wildcard" ] is False
58- assert ir ["match_mode " ] == "any"
58+ assert ir ["matchMode " ] == "any"
5959
6060 def test_to_ir_wildcard (self ):
6161 """Test CallMatcher.to_ir() with wildcard."""
@@ -126,3 +126,217 @@ def test_repr(self):
126126 """Test VariableMatcher.__repr__() output."""
127127 matcher = variable ("user_input" )
128128 assert repr (matcher ) == 'variable("user_input")'
129+
130+
131+ class TestCallMatcherKeywordArguments :
132+ """Test suite for keyword argument matching (match_name)."""
133+
134+ def test_single_keyword_arg_string (self ):
135+ """Test matching single keyword argument with string value."""
136+ matcher = calls ("app.run" , match_name = {"debug" : "True" })
137+ ir = matcher .to_ir ()
138+
139+ assert "keywordArgs" in ir
140+ assert "debug" in ir ["keywordArgs" ]
141+ assert ir ["keywordArgs" ]["debug" ]["value" ] == "True"
142+ assert ir ["keywordArgs" ]["debug" ]["wildcard" ] is False
143+
144+ def test_single_keyword_arg_boolean (self ):
145+ """Test matching keyword argument with boolean value."""
146+ matcher = calls ("app.run" , match_name = {"debug" : True })
147+ ir = matcher .to_ir ()
148+
149+ assert ir ["keywordArgs" ]["debug" ]["value" ] is True
150+
151+ def test_single_keyword_arg_number (self ):
152+ """Test matching keyword argument with numeric value."""
153+ matcher = calls ("app.listen" , match_name = {"port" : 8080 })
154+ ir = matcher .to_ir ()
155+
156+ assert ir ["keywordArgs" ]["port" ]["value" ] == 8080
157+
158+ def test_multiple_keyword_args (self ):
159+ """Test matching multiple keyword arguments."""
160+ matcher = calls (
161+ "app.run" , match_name = {"host" : "0.0.0.0" , "port" : 5000 , "debug" : True }
162+ )
163+ ir = matcher .to_ir ()
164+
165+ assert len (ir ["keywordArgs" ]) == 3
166+ assert ir ["keywordArgs" ]["host" ]["value" ] == "0.0.0.0"
167+ assert ir ["keywordArgs" ]["port" ]["value" ] == 5000
168+ assert ir ["keywordArgs" ]["debug" ]["value" ] is True
169+
170+ def test_keyword_arg_or_logic (self ):
171+ """Test matching keyword argument with multiple values (OR logic)."""
172+ matcher = calls (
173+ "yaml.load" , match_name = {"Loader" : ["Loader" , "UnsafeLoader" , "FullLoader" ]}
174+ )
175+ ir = matcher .to_ir ()
176+
177+ assert isinstance (ir ["keywordArgs" ]["Loader" ]["value" ], list )
178+ assert len (ir ["keywordArgs" ]["Loader" ]["value" ]) == 3
179+
180+
181+ class TestCallMatcherPositionalArguments :
182+ """Test suite for positional argument matching (match_position)."""
183+
184+ def test_single_positional_arg (self ):
185+ """Test matching single positional argument."""
186+ matcher = calls ("socket.bind" , match_position = {0 : "0.0.0.0" })
187+ ir = matcher .to_ir ()
188+
189+ assert "positionalArgs" in ir
190+ assert "0" in ir ["positionalArgs" ] # JSON keys are strings
191+ assert ir ["positionalArgs" ]["0" ]["value" ] == "0.0.0.0"
192+
193+ def test_multiple_positional_args (self ):
194+ """Test matching multiple positional arguments."""
195+ matcher = calls ("chmod" , match_position = {0 : "/tmp/file" , 1 : 0o777 })
196+ ir = matcher .to_ir ()
197+
198+ assert len (ir ["positionalArgs" ]) == 2
199+ assert ir ["positionalArgs" ]["0" ]["value" ] == "/tmp/file"
200+ assert ir ["positionalArgs" ]["1" ]["value" ] == 0o777
201+
202+ def test_positional_arg_or_logic (self ):
203+ """Test matching positional argument with multiple values."""
204+ matcher = calls ("open" , match_position = {1 : ["w" , "a" , "w+" , "a+" ]})
205+ ir = matcher .to_ir ()
206+
207+ assert isinstance (ir ["positionalArgs" ]["1" ]["value" ], list )
208+ assert len (ir ["positionalArgs" ]["1" ]["value" ]) == 4
209+
210+
211+ class TestCallMatcherCombinedArguments :
212+ """Test suite for combined positional and keyword argument matching."""
213+
214+ def test_both_positional_and_keyword (self ):
215+ """Test matching both positional and keyword arguments."""
216+ matcher = calls (
217+ "app.run" ,
218+ match_position = {0 : "localhost" },
219+ match_name = {"debug" : True , "port" : 5000 },
220+ )
221+ ir = matcher .to_ir ()
222+
223+ assert "positionalArgs" in ir
224+ assert "keywordArgs" in ir
225+ assert ir ["positionalArgs" ]["0" ]["value" ] == "localhost"
226+ assert ir ["keywordArgs" ]["debug" ]["value" ] is True
227+ assert ir ["keywordArgs" ]["port" ]["value" ] == 5000
228+
229+
230+ class TestCallMatcherWildcardMatching :
231+ """Test suite for wildcard matching in argument values."""
232+
233+ def test_wildcard_in_string_value (self ):
234+ """Test automatic wildcard detection in string values."""
235+ matcher = calls ("chmod" , match_position = {1 : "0o7*" })
236+ ir = matcher .to_ir ()
237+
238+ # Wildcard should be auto-detected from '*' in value
239+ assert ir ["positionalArgs" ]["1" ]["wildcard" ] is True
240+
241+ def test_wildcard_in_list_value (self ):
242+ """Test wildcard detection in list of values."""
243+ matcher = calls ("open" , match_position = {1 : ["w*" , "a*" ]})
244+ ir = matcher .to_ir ()
245+
246+ assert ir ["positionalArgs" ]["1" ]["wildcard" ] is True
247+
248+ def test_explicit_wildcard_flag (self ):
249+ """Test explicit wildcard flag propagation."""
250+ matcher = calls ("app.*" , match_name = {"host" : "192.168.1.1" })
251+ ir = matcher .to_ir ()
252+
253+ assert ir ["wildcard" ] is True
254+ # Wildcard in function pattern propagates to argument constraints
255+ assert ir ["keywordArgs" ]["host" ]["wildcard" ] is True
256+
257+
258+ class TestCallMatcherBackwardCompatibility :
259+ """Test suite for backward compatibility with existing rules."""
260+
261+ def test_no_arguments_specified (self ):
262+ """Test that rules without argument constraints still work."""
263+ matcher = calls ("eval" )
264+ ir = matcher .to_ir ()
265+
266+ # Should not have argument constraint fields
267+ assert "positionalArgs" not in ir
268+ assert "keywordArgs" not in ir
269+
270+ def test_empty_argument_dicts (self ):
271+ """Test that empty argument dicts don't add IR fields."""
272+ matcher = calls ("eval" , match_position = {}, match_name = {})
273+ ir = matcher .to_ir ()
274+
275+ # Empty dicts should not add fields
276+ assert "positionalArgs" not in ir
277+ assert "keywordArgs" not in ir
278+
279+
280+ class TestCallMatcherIRSerialization :
281+ """Test suite for JSON serialization of generated IR."""
282+
283+ def test_complex_ir_serialization (self ):
284+ """Test that complex IR can be serialized to JSON."""
285+ import json
286+
287+ matcher = calls (
288+ "app.run" ,
289+ match_position = {0 : "0.0.0.0" },
290+ match_name = {"debug" : True , "port" : 5000 , "host" : ["localhost" , "0.0.0.0" ]},
291+ )
292+ ir = matcher .to_ir ()
293+
294+ # Should be JSON-serializable
295+ json_str = json .dumps (ir )
296+ reconstructed = json .loads (json_str )
297+
298+ assert reconstructed ["type" ] == "call_matcher"
299+ assert reconstructed ["keywordArgs" ]["debug" ]["value" ] is True
300+
301+ def test_special_values_serialization (self ):
302+ """Test serialization of special Python values."""
303+ import json
304+
305+ matcher = calls ("chmod" , match_position = {1 : 0o777 })
306+ ir = matcher .to_ir ()
307+
308+ json_str = json .dumps (ir )
309+ reconstructed = json .loads (json_str )
310+
311+ # Octal should be serialized as decimal integer
312+ assert reconstructed ["positionalArgs" ]["1" ]["value" ] == 511
313+
314+
315+ class TestCallMatcherEdgeCases :
316+ """Test suite for edge cases and error conditions."""
317+
318+ def test_none_values_handled (self ):
319+ """Test that None match_name/match_position are handled."""
320+ matcher = calls ("eval" , match_name = None , match_position = None )
321+ ir = matcher .to_ir ()
322+
323+ assert "keywordArgs" not in ir
324+ assert "positionalArgs" not in ir
325+
326+ def test_mixed_value_types (self ):
327+ """Test mixing different value types in same rule."""
328+ matcher = calls (
329+ "config.set" ,
330+ match_name = {
331+ "timeout" : 30 , # int
332+ "enabled" : True , # bool
333+ "host" : "localhost" , # string
334+ "retry" : 5.5 , # float
335+ },
336+ )
337+ ir = matcher .to_ir ()
338+
339+ assert ir ["keywordArgs" ]["timeout" ]["value" ] == 30
340+ assert ir ["keywordArgs" ]["enabled" ]["value" ] is True
341+ assert ir ["keywordArgs" ]["host" ]["value" ] == "localhost"
342+ assert ir ["keywordArgs" ]["retry" ]["value" ] == 5.5
0 commit comments