Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions sourcecode-parser/dsl/call_matcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
201 changes: 201 additions & 0 deletions sourcecode-parser/dsl/call_matcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
17 changes: 17 additions & 0 deletions sourcecode-parser/dsl/ir_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading