diff --git a/internal/engine/declaration_test.go b/internal/engine/declaration_test.go index 7120f62..520f115 100644 --- a/internal/engine/declaration_test.go +++ b/internal/engine/declaration_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/HarshK97/diffmantic/internal/testutil" + "github.com/HarshK97/diffmantic/internal/treesitter" ) func TestGetDeclarationName(t *testing.T) { @@ -330,3 +331,127 @@ func TestMatchDeclarationIntegration(t *testing.T) { t.Error("declaration pre-match should map foo -> foo") } } + +func TestBottomUpDeclarationNameAffinity(t *testing.T) { + // BottomUp should skip c1 because its declaration name doesn't match t1. + leafA1 := testutil.Leaf("id", "x") + leafA2 := testutil.Leaf("id", "x") + leafB1 := testutil.Leaf("id", "y") + leafB2 := testutil.Leaf("id", "y") + + t1 := testutil.Node("function_declaration", "", + testutil.Leaf("identifier", "foo"), + leafA1, + leafB1, + ) + t1.Language = "go" + + c1 := testutil.Node("function_declaration", "", + testutil.Leaf("identifier", "bar"), + leafA2, + ) + c1.Language = "go" + + c2 := testutil.Node("function_declaration", "", + testutil.Leaf("identifier", "foo"), + leafB2, + ) + c2.Language = "go" + + m := NewMapping() + m.Add(leafA1, leafA2) + m.Add(leafB1, leafB2) + + picked := candidate(t1, []*treesitter.ASTNode{c1, c2}, m) + if picked != c2 { + t.Errorf("candidate() selected %v, want c2 (%v)", picked, c2) + } +} + +func TestMatchDeclarationsEquivalentTypes(t *testing.T) { + t.Run("matches function_declaration with variable_declaration in lua", func(t *testing.T) { + src := testutil.Node("chunk", "", + testutil.Node("function_declaration", "", + testutil.Leaf("identifier", "sync_once_impl"), + testutil.Node("block", "", + testutil.Leaf("return_statement", "return"), + ), + ), + ) + src.Language = "lua" + + dst := testutil.Node("chunk", "", + testutil.Node("variable_declaration", "", + testutil.Leaf("identifier", "sync_once_impl"), + testutil.Node("block", "", + testutil.Leaf("return_statement", "return"), + ), + ), + ) + dst.Language = "lua" + + m := NewMapping() + matchDeclarations(src, dst, m) + + srcFn := src.Children[0] + dstFn := dst.Children[0] + if !m.Has(srcFn) { + t.Fatal("src function_declaration should map to dst variable_declaration under equivalent types") + } + if m.Src()[srcFn] != dstFn { + t.Errorf("expected %v to map to %v, got %v", srcFn, dstFn, m.Src()[srcFn]) + } + + srcBlock := srcFn.Children[1] + dstBlock := dstFn.Children[1] + if m.Src()[srcBlock] != dstBlock { + t.Errorf("expected body blocks to be matched and recovered") + } + }) + + t.Run("disambiguates forward declaration vs full definition by size", func(t *testing.T) { + srcForward := testutil.Node("variable_declaration", "", + testutil.Leaf("identifier", "sync_once_impl"), + ) + srcForward.Language = "lua" + + srcFull := testutil.Node("function_declaration", "", + testutil.Leaf("identifier", "sync_once_impl"), + testutil.Node("parameters", ""), + testutil.Node("block", "", + testutil.Leaf("statement", "a"), + testutil.Leaf("statement", "b"), + testutil.Leaf("statement", "c"), + ), + ) + srcFull.Language = "lua" + + src := testutil.Node("chunk", "", srcForward, srcFull) + src.Language = "lua" + + dstFull := testutil.Node("function_declaration", "", + testutil.Leaf("identifier", "sync_once_impl"), + testutil.Node("parameters", ""), + testutil.Node("block", "", + testutil.Leaf("statement", "a"), + testutil.Leaf("statement", "b"), + ), + ) + dstFull.Language = "lua" + dst := testutil.Node("chunk", "", dstFull) + dst.Language = "lua" + + m := NewMapping() + matchDeclarations(src, dst, m) + + if m.Has(srcForward) { + t.Errorf("forward declaration should not steal destination function") + } + if !m.Has(srcFull) { + t.Fatalf("full definition should map to destination function") + } + if m.Src()[srcFull] != dstFull { + t.Errorf("expected srcFull to map to dstFull") + } + }) +} diff --git a/internal/engine/matcher.go b/internal/engine/matcher.go index d5d8106..f92b3e3 100644 --- a/internal/engine/matcher.go +++ b/internal/engine/matcher.go @@ -424,20 +424,90 @@ func matchDeclarations(t1Root, t2Root *treesitter.ASTNode, m *Mapping) { } } - for key, d2List := range t2Map { + visited := make(map[decKey]bool, len(t2Map)) + for _, d2 := range t2Decs { + if m.HasDst(d2) { + continue + } + key := decKey{name: getDeclarationName(d2), rec: getReceiverTypeName(d2)} + if key.name == "" || visited[key] { + continue + } + visited[key] = true + + d2List := t2Map[key] if len(d2List) != 1 { continue } d1List, ok := t1Map[key] - if !ok || len(d1List) != 1 { + if !ok || len(d1List) == 0 { continue } - d1 := d1List[0] - d2 := d2List[0] - if TypesMatch(d1.Type, d2.Type, rules) { - m.Add(d1, d2) + + var matchingD1s []*treesitter.ASTNode + for _, d1 := range d1List { + if TypesMatch(d1.Type, d2.Type, rules) { + matchingD1s = append(matchingD1s, d1) + } } + + if len(matchingD1s) > 0 { + bestD1 := matchingD1s[0] + if len(matchingD1s) > 1 { + // If one is a forward decl and the other is a full definition, + // pick the one whose subtree size is closest to d2. + d2Size := d2.Size() + bestDiff := max(bestD1.Size()-d2Size, d2Size-bestD1.Size()) + for _, cand := range matchingD1s[1:] { + if diff := max(cand.Size()-d2Size, d2Size-cand.Size()); diff < bestDiff { + bestDiff = diff + bestD1 = cand + } + } + } + m.Add(bestD1, d2) + matchDeclarationBodies(bestD1, d2, m, rules) + } + } +} + +func matchDeclarationBodies(d1, d2 *treesitter.ASTNode, m *Mapping, rules *treesitter.Rules) { + if d1 == nil || d2 == nil || m == nil { + return + } + var b1, b2 *treesitter.ASTNode + for _, c1 := range d1.Children { + if isBlockNode(c1, rules) { + b1 = c1 + break + } + } + for _, c2 := range d2.Children { + if isBlockNode(c2, rules) { + b2 = c2 + break + } + } + if b1 != nil && b2 != nil { + if cur := m.Dst()[b2]; cur != nil && cur != b1 { + m.Remove(cur) + } + if cur := m.Src()[b1]; cur != nil && cur != b2 { + m.Remove(b1) + } + m.Add(b1, b2) + Recover(b1, b2, m) + } +} + +func isBlockNode(n *treesitter.ASTNode, rules *treesitter.Rules) bool { + if n == nil { + return false + } + if rules != nil && len(rules.Blocks) > 0 { + return slices.Contains(rules.Blocks, n.Type) } + return n.Type == "block" } func findDeclarations(root *treesitter.ASTNode) []*treesitter.ASTNode { diff --git a/internal/treesitter/c/rules.yml b/internal/treesitter/c/rules.yml index dc35620..6d37574 100644 --- a/internal/treesitter/c/rules.yml +++ b/internal/treesitter/c/rules.yml @@ -159,6 +159,9 @@ declarations: identifiers: - identifier +blocks: + - compound_statement + pairs: - field_designator diff --git a/internal/treesitter/cpp/rules.yml b/internal/treesitter/cpp/rules.yml index da71b8e..213ec05 100644 --- a/internal/treesitter/cpp/rules.yml +++ b/internal/treesitter/cpp/rules.yml @@ -214,6 +214,9 @@ identifiers: - destructor_name - operator_name +blocks: + - compound_statement + pairs: - field_designator diff --git a/internal/treesitter/go/rules.yml b/internal/treesitter/go/rules.yml index 40f3c85..7b037cd 100644 --- a/internal/treesitter/go/rules.yml +++ b/internal/treesitter/go/rules.yml @@ -130,6 +130,9 @@ identifiers: - type_identifier - package_identifier +blocks: + - block + unordered: - interface_type diff --git a/internal/treesitter/java/rules.yml b/internal/treesitter/java/rules.yml index 68cbf79..5f468f7 100644 --- a/internal/treesitter/java/rules.yml +++ b/internal/treesitter/java/rules.yml @@ -190,6 +190,10 @@ declarations: identifiers: - identifier +blocks: + - block + - constructor_body + unordered: - annotation diff --git a/internal/treesitter/javascript/rules.yml b/internal/treesitter/javascript/rules.yml index 7f7f945..68231bf 100644 --- a/internal/treesitter/javascript/rules.yml +++ b/internal/treesitter/javascript/rules.yml @@ -177,6 +177,9 @@ identifiers: - identifier - private_property_identifier +blocks: + - statement_block + unordered: - object diff --git a/internal/treesitter/lua/rules.yml b/internal/treesitter/lua/rules.yml index 7c00856..3af93ff 100644 --- a/internal/treesitter/lua/rules.yml +++ b/internal/treesitter/lua/rules.yml @@ -113,6 +113,9 @@ declarations: identifiers: - identifier +blocks: + - block + pairs: - field diff --git a/internal/treesitter/php/rules.yml b/internal/treesitter/php/rules.yml index 695f135..732a35a 100644 --- a/internal/treesitter/php/rules.yml +++ b/internal/treesitter/php/rules.yml @@ -175,6 +175,9 @@ identifiers: - name - variable_name +blocks: + - compound_statement + unordered: - array_creation_expression diff --git a/internal/treesitter/python/rules.yml b/internal/treesitter/python/rules.yml index 4f5a99c..e9834b8 100644 --- a/internal/treesitter/python/rules.yml +++ b/internal/treesitter/python/rules.yml @@ -158,6 +158,9 @@ declarations: identifiers: - identifier +blocks: + - block + unordered: - dictionary diff --git a/internal/treesitter/ruby/rules.yml b/internal/treesitter/ruby/rules.yml index 64f03f6..cc29626 100644 --- a/internal/treesitter/ruby/rules.yml +++ b/internal/treesitter/ruby/rules.yml @@ -159,6 +159,9 @@ identifiers: - identifier - constant +blocks: + - block + unordered: - hash diff --git a/internal/treesitter/rules.go b/internal/treesitter/rules.go index f780301..a54ec2a 100644 --- a/internal/treesitter/rules.go +++ b/internal/treesitter/rules.go @@ -17,6 +17,7 @@ type Rules struct { Keywords []string `yaml:"keywords"` Declarations []string `yaml:"declarations"` Identifiers []string `yaml:"identifiers"` + Blocks []string `yaml:"blocks"` Pairs []string `yaml:"pairs"` Unordered []string `yaml:"unordered"` EquivalentTypes [][]string `yaml:"equivalent_types"` diff --git a/internal/treesitter/rules_test.go b/internal/treesitter/rules_test.go index 19d2d2a..488345c 100644 --- a/internal/treesitter/rules_test.go +++ b/internal/treesitter/rules_test.go @@ -309,3 +309,36 @@ func TestEveryLanguageIdentifiersAreValidSymbols(t *testing.T) { } } } + +func TestEveryLanguageBlocksAreValidSymbols(t *testing.T) { + for _, ext := range []string{ + "c.c", "cpp.cc", "css.css", "go.go", "html.html", "java.java", + "javascript.js", "json.json", "lua.lua", "php.php", "python.py", + "ruby.rb", "rust.rs", "toml.toml", "tsx.tsx", "typescript.ts", + "yaml.yaml", "zig.zig", + } { + entry := DetectGrammarEntry(ext) + if entry == nil { + continue + } + lang := entry.Language() + namedSymbols := make(map[string]bool) + for i := 0; i < int(lang.SymbolCount) && i < len(lang.SymbolNames); i++ { + name := lang.SymbolNames[i] + isNamed := i < len(lang.SymbolMetadata) && lang.SymbolMetadata[i].Named + if name != "" && isNamed { + namedSymbols[name] = true + } + } + + rules := GetRules(entry.Name) + if rules == nil { + continue + } + for _, sym := range rules.Blocks { + if !namedSymbols[sym] { + t.Errorf("language %s: blocks symbol %q is not a valid named symbol in grammar", entry.Name, sym) + } + } + } +} diff --git a/internal/treesitter/rust/rules.yml b/internal/treesitter/rust/rules.yml index 4ee4864..24fa79b 100644 --- a/internal/treesitter/rust/rules.yml +++ b/internal/treesitter/rust/rules.yml @@ -154,6 +154,9 @@ declarations: identifiers: - identifier +blocks: + - block + unordered: - use_declaration diff --git a/internal/treesitter/tsx/rules.yml b/internal/treesitter/tsx/rules.yml index cc1f7fc..d25b2d4 100644 --- a/internal/treesitter/tsx/rules.yml +++ b/internal/treesitter/tsx/rules.yml @@ -195,6 +195,9 @@ identifiers: - nested_type_identifier - private_property_identifier +blocks: + - statement_block + unordered: - object diff --git a/internal/treesitter/typescript/rules.yml b/internal/treesitter/typescript/rules.yml index f9f6a5d..9723306 100644 --- a/internal/treesitter/typescript/rules.yml +++ b/internal/treesitter/typescript/rules.yml @@ -181,6 +181,9 @@ identifiers: - nested_type_identifier - private_property_identifier +blocks: + - statement_block + unordered: - object diff --git a/internal/treesitter/zig/rules.yml b/internal/treesitter/zig/rules.yml index 9637eac..8b57d2c 100644 --- a/internal/treesitter/zig/rules.yml +++ b/internal/treesitter/zig/rules.yml @@ -134,6 +134,9 @@ declarations: identifiers: - identifier +blocks: + - block + pairs: - initializer_pair diff --git a/tests/testdata/go_gin_fix_lint/expected_actions.json.gz b/tests/testdata/go_gin_fix_lint/expected_actions.json.gz index 72bb599..a488ddc 100644 Binary files a/tests/testdata/go_gin_fix_lint/expected_actions.json.gz and b/tests/testdata/go_gin_fix_lint/expected_actions.json.gz differ diff --git a/tests/testdata/go_gin_fix_lint/expected_ui.json.gz b/tests/testdata/go_gin_fix_lint/expected_ui.json.gz index cd6d8de..03d2e13 100644 Binary files a/tests/testdata/go_gin_fix_lint/expected_ui.json.gz and b/tests/testdata/go_gin_fix_lint/expected_ui.json.gz differ diff --git a/tests/testdata/java_commons_lang_simplify_abstractreflection_setaccessible/expected_actions.json.gz b/tests/testdata/java_commons_lang_simplify_abstractreflection_setaccessible/expected_actions.json.gz index 7a70bfc..243ba7e 100644 Binary files a/tests/testdata/java_commons_lang_simplify_abstractreflection_setaccessible/expected_actions.json.gz and b/tests/testdata/java_commons_lang_simplify_abstractreflection_setaccessible/expected_actions.json.gz differ diff --git a/tests/testdata/java_commons_lang_simplify_abstractreflection_setaccessible/expected_ui.json.gz b/tests/testdata/java_commons_lang_simplify_abstractreflection_setaccessible/expected_ui.json.gz index f978010..47442e4 100644 Binary files a/tests/testdata/java_commons_lang_simplify_abstractreflection_setaccessible/expected_ui.json.gz and b/tests/testdata/java_commons_lang_simplify_abstractreflection_setaccessible/expected_ui.json.gz differ diff --git a/tests/testdata/java_mockito_non_deterministic_assertionerror/expected_actions.json.gz b/tests/testdata/java_mockito_non_deterministic_assertionerror/expected_actions.json.gz index 3811373..8996c54 100644 Binary files a/tests/testdata/java_mockito_non_deterministic_assertionerror/expected_actions.json.gz and b/tests/testdata/java_mockito_non_deterministic_assertionerror/expected_actions.json.gz differ diff --git a/tests/testdata/java_mockito_non_deterministic_assertionerror/expected_ui.json.gz b/tests/testdata/java_mockito_non_deterministic_assertionerror/expected_ui.json.gz index 1537bc8..c249459 100644 Binary files a/tests/testdata/java_mockito_non_deterministic_assertionerror/expected_ui.json.gz and b/tests/testdata/java_mockito_non_deterministic_assertionerror/expected_ui.json.gz differ diff --git a/tests/testdata/lua_kong_sync_retry_timeout/expected_actions.json.gz b/tests/testdata/lua_kong_sync_retry_timeout/expected_actions.json.gz index 0eda3e6..67a3f88 100644 Binary files a/tests/testdata/lua_kong_sync_retry_timeout/expected_actions.json.gz and b/tests/testdata/lua_kong_sync_retry_timeout/expected_actions.json.gz differ diff --git a/tests/testdata/lua_kong_sync_retry_timeout/expected_ui.json.gz b/tests/testdata/lua_kong_sync_retry_timeout/expected_ui.json.gz index 2c3ac26..ce566cd 100644 Binary files a/tests/testdata/lua_kong_sync_retry_timeout/expected_ui.json.gz and b/tests/testdata/lua_kong_sync_retry_timeout/expected_ui.json.gz differ diff --git a/tests/testdata/php_guzzle_handler_curl_multi/expected_actions.json.gz b/tests/testdata/php_guzzle_handler_curl_multi/expected_actions.json.gz index 9fa3d6c..1ade028 100644 Binary files a/tests/testdata/php_guzzle_handler_curl_multi/expected_actions.json.gz and b/tests/testdata/php_guzzle_handler_curl_multi/expected_actions.json.gz differ diff --git a/tests/testdata/php_guzzle_handler_curl_multi/expected_ui.json.gz b/tests/testdata/php_guzzle_handler_curl_multi/expected_ui.json.gz index f511691..d2f46b7 100644 Binary files a/tests/testdata/php_guzzle_handler_curl_multi/expected_ui.json.gz and b/tests/testdata/php_guzzle_handler_curl_multi/expected_ui.json.gz differ diff --git a/tests/testdata/php_uuid_guid_fields_extract/expected_actions.json.gz b/tests/testdata/php_uuid_guid_fields_extract/expected_actions.json.gz index 060119d..456f7a8 100644 Binary files a/tests/testdata/php_uuid_guid_fields_extract/expected_actions.json.gz and b/tests/testdata/php_uuid_guid_fields_extract/expected_actions.json.gz differ diff --git a/tests/testdata/php_uuid_guid_fields_extract/expected_ui.json.gz b/tests/testdata/php_uuid_guid_fields_extract/expected_ui.json.gz index 8c6d224..fdb64c7 100644 Binary files a/tests/testdata/php_uuid_guid_fields_extract/expected_ui.json.gz and b/tests/testdata/php_uuid_guid_fields_extract/expected_ui.json.gz differ diff --git a/tests/testdata/py_requests_align_sessionget/expected_actions.json.gz b/tests/testdata/py_requests_align_sessionget/expected_actions.json.gz index ca386be..4eb7038 100644 Binary files a/tests/testdata/py_requests_align_sessionget/expected_actions.json.gz and b/tests/testdata/py_requests_align_sessionget/expected_actions.json.gz differ diff --git a/tests/testdata/py_requests_align_sessionget/expected_ui.json.gz b/tests/testdata/py_requests_align_sessionget/expected_ui.json.gz index 8638c65..d24142b 100644 Binary files a/tests/testdata/py_requests_align_sessionget/expected_ui.json.gz and b/tests/testdata/py_requests_align_sessionget/expected_ui.json.gz differ diff --git a/tests/testdata/py_requests_refactor_prefer/expected_actions.json.gz b/tests/testdata/py_requests_refactor_prefer/expected_actions.json.gz index 58f9cfd..7964224 100644 Binary files a/tests/testdata/py_requests_refactor_prefer/expected_actions.json.gz and b/tests/testdata/py_requests_refactor_prefer/expected_actions.json.gz differ diff --git a/tests/testdata/py_requests_refactor_prefer/expected_ui.json.gz b/tests/testdata/py_requests_refactor_prefer/expected_ui.json.gz index 9d4e992..80f0bf0 100644 Binary files a/tests/testdata/py_requests_refactor_prefer/expected_ui.json.gz and b/tests/testdata/py_requests_refactor_prefer/expected_ui.json.gz differ diff --git a/tests/testdata/rust_tokio_macros_trait_method/expected_actions.json.gz b/tests/testdata/rust_tokio_macros_trait_method/expected_actions.json.gz index c2afb22..0f8027e 100644 Binary files a/tests/testdata/rust_tokio_macros_trait_method/expected_actions.json.gz and b/tests/testdata/rust_tokio_macros_trait_method/expected_actions.json.gz differ diff --git a/tests/testdata/rust_tokio_macros_trait_method/expected_ui.json.gz b/tests/testdata/rust_tokio_macros_trait_method/expected_ui.json.gz index 8c1df77..822a8c2 100644 Binary files a/tests/testdata/rust_tokio_macros_trait_method/expected_ui.json.gz and b/tests/testdata/rust_tokio_macros_trait_method/expected_ui.json.gz differ diff --git a/tests/testdata/rust_tokio_test_tests_when/expected_actions.json.gz b/tests/testdata/rust_tokio_test_tests_when/expected_actions.json.gz index b007cd2..e985a9b 100644 Binary files a/tests/testdata/rust_tokio_test_tests_when/expected_actions.json.gz and b/tests/testdata/rust_tokio_test_tests_when/expected_actions.json.gz differ diff --git a/tests/testdata/rust_tokio_test_tests_when/expected_ui.json.gz b/tests/testdata/rust_tokio_test_tests_when/expected_ui.json.gz index 31b2d26..af65b32 100644 Binary files a/tests/testdata/rust_tokio_test_tests_when/expected_ui.json.gz and b/tests/testdata/rust_tokio_test_tests_when/expected_ui.json.gz differ diff --git a/tests/testdata/rust_tokio_time_loom_test/expected_actions.json.gz b/tests/testdata/rust_tokio_time_loom_test/expected_actions.json.gz index 2446d8d..0b81097 100644 Binary files a/tests/testdata/rust_tokio_time_loom_test/expected_actions.json.gz and b/tests/testdata/rust_tokio_time_loom_test/expected_actions.json.gz differ diff --git a/tests/testdata/rust_tokio_time_loom_test/expected_ui.json.gz b/tests/testdata/rust_tokio_time_loom_test/expected_ui.json.gz index 48f33da..c6c5deb 100644 Binary files a/tests/testdata/rust_tokio_time_loom_test/expected_ui.json.gz and b/tests/testdata/rust_tokio_time_loom_test/expected_ui.json.gz differ diff --git a/tests/testdata/zig_clap_new_struct_tuple/expected_actions.json.gz b/tests/testdata/zig_clap_new_struct_tuple/expected_actions.json.gz index e2f8277..eae8298 100644 Binary files a/tests/testdata/zig_clap_new_struct_tuple/expected_actions.json.gz and b/tests/testdata/zig_clap_new_struct_tuple/expected_actions.json.gz differ diff --git a/tests/testdata/zig_clap_new_struct_tuple/expected_ui.json.gz b/tests/testdata/zig_clap_new_struct_tuple/expected_ui.json.gz index 7623bf4..5b0bb74 100644 Binary files a/tests/testdata/zig_clap_new_struct_tuple/expected_ui.json.gz and b/tests/testdata/zig_clap_new_struct_tuple/expected_ui.json.gz differ