diff --git a/sourcecode-parser/dsl/call_matcher.go b/sourcecode-parser/dsl/call_matcher.go index 3b5aaa39..53b6a5a2 100644 --- a/sourcecode-parser/dsl/call_matcher.go +++ b/sourcecode-parser/dsl/call_matcher.go @@ -140,3 +140,42 @@ func (e *CallMatcherExecutor) getMatchedPattern(cs *core.CallSite) string { } return "" } + +// parseKeywordArguments extracts keyword arguments from CallSite.Arguments. +// +// Example: +// +// Input: []Argument{ +// {Value: "x", Position: 0}, +// {Value: "y=2", Position: 1}, +// {Value: "debug=True", Position: 2} +// } +// Output: map[string]string{ +// "y": "2", +// "debug": "True" +// } +// +// Algorithm: +// 1. Iterate through all arguments +// 2. Check if argument contains "=" (keyword arg format) +// 3. Split by "=" to get name and value +// 4. Trim whitespace and store in map +// +// Performance: O(N) where N = number of arguments (~2-5 typically). +func (e *CallMatcherExecutor) parseKeywordArguments(args []core.Argument) map[string]string { + kwargs := make(map[string]string) + + for _, arg := range args { + // Check if argument is in "key=value" format + if strings.Contains(arg.Value, "=") { + parts := strings.SplitN(arg.Value, "=", 2) + if len(parts) == 2 { + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + kwargs[key] = value + } + } + } + + return kwargs +} diff --git a/sourcecode-parser/dsl/call_matcher_test.go b/sourcecode-parser/dsl/call_matcher_test.go index 4806af49..8286e243 100644 --- a/sourcecode-parser/dsl/call_matcher_test.go +++ b/sourcecode-parser/dsl/call_matcher_test.go @@ -144,3 +144,204 @@ func BenchmarkCallMatcherExecutor(b *testing.B) { executor.Execute() } } + +// TestParseKeywordArguments_EmptyArgs tests parsing with no arguments. +func TestParseKeywordArguments_EmptyArgs(t *testing.T) { + executor := &CallMatcherExecutor{ + IR: &CallMatcherIR{}, + } + + args := []core.Argument{} + result := executor.parseKeywordArguments(args) + + assert.Empty(t, result, "Expected empty map for empty arguments") +} + +// TestParseKeywordArguments_PositionalOnly tests parsing with only positional args. +func TestParseKeywordArguments_PositionalOnly(t *testing.T) { + executor := &CallMatcherExecutor{ + IR: &CallMatcherIR{}, + } + + args := []core.Argument{ + {Value: "x", Position: 0}, + {Value: "y", Position: 1}, + } + result := executor.parseKeywordArguments(args) + + assert.Empty(t, result, "Expected empty map for positional-only arguments") +} + +// TestParseKeywordArguments_SingleKeyword tests parsing single keyword argument. +func TestParseKeywordArguments_SingleKeyword(t *testing.T) { + executor := &CallMatcherExecutor{ + IR: &CallMatcherIR{}, + } + + args := []core.Argument{ + {Value: "debug=True", Position: 0}, + } + result := executor.parseKeywordArguments(args) + + assert.Len(t, result, 1, "Expected 1 keyword argument") + assert.Equal(t, "True", result["debug"], "Expected debug=True") +} + +// TestParseKeywordArguments_MultipleKeywords tests parsing multiple keyword args. +func TestParseKeywordArguments_MultipleKeywords(t *testing.T) { + executor := &CallMatcherExecutor{ + IR: &CallMatcherIR{}, + } + + args := []core.Argument{ + {Value: "host=\"0.0.0.0\"", Position: 0}, + {Value: "port=5000", Position: 1}, + {Value: "debug=True", Position: 2}, + } + result := executor.parseKeywordArguments(args) + + expected := map[string]string{ + "host": "\"0.0.0.0\"", + "port": "5000", + "debug": "True", + } + + assert.Len(t, result, len(expected), "Expected %d keyword arguments", len(expected)) + for key, expectedValue := range expected { + assert.Equal(t, expectedValue, result[key], "Expected %s=%s", key, expectedValue) + } +} + +// TestParseKeywordArguments_MixedArgs tests parsing mixed positional and keyword args. +func TestParseKeywordArguments_MixedArgs(t *testing.T) { + executor := &CallMatcherExecutor{ + IR: &CallMatcherIR{}, + } + + args := []core.Argument{ + {Value: "app", Position: 0}, // Positional + {Value: "host=\"0.0.0.0\"", Position: 1}, // Keyword + {Value: "5000", Position: 2}, // Positional + {Value: "debug=True", Position: 3}, // Keyword + } + result := executor.parseKeywordArguments(args) + + expected := map[string]string{ + "host": "\"0.0.0.0\"", + "debug": "True", + } + + assert.Len(t, result, len(expected), "Expected %d keyword arguments", len(expected)) + for key, expectedValue := range expected { + assert.Equal(t, expectedValue, result[key], "Expected %s=%s", key, expectedValue) + } +} + +// TestParseKeywordArguments_WithWhitespace tests parsing with whitespace. +func TestParseKeywordArguments_WithWhitespace(t *testing.T) { + executor := &CallMatcherExecutor{ + IR: &CallMatcherIR{}, + } + + args := []core.Argument{ + {Value: " debug = True ", Position: 0}, + {Value: "host = \"0.0.0.0\"", Position: 1}, + } + result := executor.parseKeywordArguments(args) + + expected := map[string]string{ + "debug": "True", + "host": "\"0.0.0.0\"", + } + + assert.Len(t, result, len(expected), "Expected %d keyword arguments", len(expected)) + for key, expectedValue := range expected { + assert.Equal(t, expectedValue, result[key], "Expected %s=%s (whitespace should be trimmed)", key, expectedValue) + } +} + +// TestParseKeywordArguments_ComplexValues tests parsing complex values. +func TestParseKeywordArguments_ComplexValues(t *testing.T) { + executor := &CallMatcherExecutor{ + IR: &CallMatcherIR{}, + } + + args := []core.Argument{ + {Value: "config={\"key\": \"value\"}", Position: 0}, + {Value: "url=\"http://example.com/path?param=value\"", Position: 1}, + } + result := executor.parseKeywordArguments(args) + + assert.Len(t, result, 2, "Expected 2 keyword arguments") + assert.Equal(t, "{\"key\": \"value\"}", result["config"], "Complex value not parsed correctly") + assert.Equal(t, "\"http://example.com/path?param=value\"", result["url"], "URL with = not parsed correctly") +} + +// TestParseKeywordArguments_EdgeCases tests edge cases. +func TestParseKeywordArguments_EdgeCases(t *testing.T) { + executor := &CallMatcherExecutor{ + IR: &CallMatcherIR{}, + } + + // Test with malformed arguments (should skip or include based on behavior) + args := []core.Argument{ + {Value: "=value", Position: 0}, // Missing key + {Value: "key=", Position: 1}, // Missing value (should include) + {Value: "validkey=validvalue", Position: 2}, + } + result := executor.parseKeywordArguments(args) + + // Should parse "key=" as key with empty value, and "validkey=validvalue" + // "=value" should be skipped as it has empty key after trim + assert.Contains(t, result, "key", "Should parse key with empty value") + assert.Contains(t, result, "validkey", "Should parse valid key=value") + assert.Equal(t, "", result["key"], "Key with no value should have empty string") + assert.Equal(t, "validvalue", result["validkey"], "Valid key=value should be parsed") +} + +// TestArgumentConstraint_StructUsage tests ArgumentConstraint struct usage. +func TestArgumentConstraint_StructUsage(t *testing.T) { + constraint := ArgumentConstraint{ + Value: "0.0.0.0", + Wildcard: false, + } + + // Test that it can be used in CallMatcherIR + ir := CallMatcherIR{ + Type: "call_matcher", + Patterns: []string{"app.run"}, + KeywordArgs: map[string]ArgumentConstraint{ + "host": constraint, + }, + } + + // Verify the struct is valid + assert.Equal(t, "0.0.0.0", ir.KeywordArgs["host"].Value, "ArgumentConstraint not stored correctly") + assert.False(t, ir.KeywordArgs["host"].Wildcard, "Wildcard should be false") +} + +// TestCallMatcherIR_BackwardCompatibility tests that IR without KeywordArgs still works. +func TestCallMatcherIR_BackwardCompatibility(t *testing.T) { + // Old IR without KeywordArgs field + ir := CallMatcherIR{ + Type: "call_matcher", + Patterns: []string{"eval", "exec"}, + Wildcard: false, + MatchMode: "any", + } + + // Should work fine (KeywordArgs is nil/empty) + assert.Nil(t, ir.KeywordArgs, "Expected nil KeywordArgs for backward compatibility") + + // Verify it can still be used with executor + cg := core.NewCallGraph() + cg.CallSites["test.main"] = []core.CallSite{ + {Target: "eval", Location: core.Location{File: "test.py", Line: 10}}, + } + + executor := NewCallMatcherExecutor(&ir, cg) + matches := executor.Execute() + + assert.Len(t, matches, 1, "Old IR should still work") + assert.Equal(t, "eval", matches[0].Target, "Old IR should match correctly") +} diff --git a/sourcecode-parser/dsl/ir_types.go b/sourcecode-parser/dsl/ir_types.go index 2fad49c7..db40a514 100644 --- a/sourcecode-parser/dsl/ir_types.go +++ b/sourcecode-parser/dsl/ir_types.go @@ -17,12 +17,29 @@ type MatcherIR interface { GetType() IRType } +// ArgumentConstraint represents a constraint on a single argument value. +type ArgumentConstraint struct { + // Value is the expected argument value(s). + // Can be a single value or a list of acceptable values (OR logic). + // Examples: "0.0.0.0", "true", "777", ["Loader", "UnsafeLoader"] + Value interface{} `json:"value"` + + // Wildcard enables pattern matching with * and ? in Value. + // Example: "0o7*" matches "0o777", "0o755", etc. + Wildcard bool `json:"wildcard"` +} + // CallMatcherIR represents call_matcher JSON IR. type CallMatcherIR struct { Type string `json:"type"` // "call_matcher" Patterns []string `json:"patterns"` // ["eval", "exec"] Wildcard bool `json:"wildcard"` // true if any pattern has * MatchMode string `json:"matchMode"` // "any" (OR) or "all" (AND) + + // KeywordArgs maps keyword argument name to expected value(s). + // Example: {"debug": ArgumentConstraint{Value: true}} + // This field is optional and will be omitted from JSON if empty. + KeywordArgs map[string]ArgumentConstraint `json:"keywordArgs,omitempty"` } // GetType returns the IR type.