diff --git a/internal/actions/chawathe.go b/internal/actions/chawathe.go index 251c157..d6f083d 100644 --- a/internal/actions/chawathe.go +++ b/internal/actions/chawathe.go @@ -217,6 +217,10 @@ func (s *chawatheState) findPos(x *treesitter.ASTNode) int { } func (s *chawatheState) alignChildren(w *cnode, x *treesitter.ASTNode) { + if w == nil || x == nil { + return + } + for _, c := range w.children { delete(s.srcInOrder, c) } @@ -242,6 +246,16 @@ func (s *chawatheState) alignChildren(w *cnode, x *treesitter.ASTNode) { } } + if x.IsUnordered || (w.orig != nil && w.orig.IsUnordered) { + for _, b := range s2 { + if a, ok := s.cpyDstToSrc[b]; ok { + s.srcInOrder[a] = true + s.dstInOrder[b] = true + } + } + return + } + lcsPairs := s.lcs(s1, s2) lcsSet := make(map[*cnode]bool) diff --git a/internal/actions/chawathe_test.go b/internal/actions/chawathe_test.go new file mode 100644 index 0000000..8d9e159 --- /dev/null +++ b/internal/actions/chawathe_test.go @@ -0,0 +1,43 @@ +package actions + +import ( + "testing" + + "github.com/HarshK97/diffmantic/internal/engine" + "github.com/HarshK97/diffmantic/internal/treesitter" +) + +func TestUnorderedNodeMatching(t *testing.T) { + // Create two container nodes with reordered child nodes + childA1 := &treesitter.ASTNode{Type: "pair", Label: "a"} + childB1 := &treesitter.ASTNode{Type: "pair", Label: "b"} + src := &treesitter.ASTNode{ + Type: "object", + IsUnordered: true, + Children: []*treesitter.ASTNode{childA1, childB1}, + } + childA1.Parent = src + childB1.Parent = src + + childB2 := &treesitter.ASTNode{Type: "pair", Label: "b"} + childA2 := &treesitter.ASTNode{Type: "pair", Label: "a"} + dst := &treesitter.ASTNode{ + Type: "object", + IsUnordered: true, + Children: []*treesitter.ASTNode{childB2, childA2}, + } + childB2.Parent = dst + childA2.Parent = dst + + ms := engine.NewMapping() + ms.Add(src, dst) + ms.Add(childA1, childA2) + ms.Add(childB1, childB2) + + script := GenerateEditScript(src, dst, ms) + for _, action := range script.Actions() { + if action.Type == Move { + t.Errorf("expected 0 Move actions for unordered container, got Move action on node %s", action.Node.Label) + } + } +} diff --git a/internal/engine/bottom-up.go b/internal/engine/bottom-up.go index b623c53..008742d 100644 --- a/internal/engine/bottom-up.go +++ b/internal/engine/bottom-up.go @@ -103,8 +103,10 @@ func candidate( bestDice := -1.0 bestLabelScore := -1 var bestSamePositional bool + var bestKeyMatched bool t1Labels := t1.LeafLabels() + t1Key := getKeyLabel(t1) for _, c := range candidates { if m.HasDst(c) { @@ -138,8 +140,14 @@ func candidate( diff := sim - bestSim isBetter := false ls := labelOverlap(t1Labels, c) + cKey := getKeyLabel(c) + keyMatched := t1Key != "" && cKey != "" && t1Key == cKey - if math.Abs(diff) > 0.05 { + if t1Key != "" && keyMatched != bestKeyMatched && t1.IsUnordered { + if keyMatched { + isBetter = true + } + } else if math.Abs(diff) > 0.05 { if sim > bestSim { isBetter = true } @@ -169,11 +177,28 @@ func candidate( best = c bestLabelScore = ls bestSamePositional = samePositional + bestKeyMatched = keyMatched } } return best } +func getKeyLabel(n *treesitter.ASTNode) string { + if n == nil || len(n.Children) == 0 { + return "" + } + k := n.Children[0] + if k.Label != "" { + return k.Label + } + for _, desc := range k.Descendants() { + if desc.Label != "" { + return desc.Label + } + } + return "" +} + // labelOverlap returns the number of shared leaf labels in t2's subtree. func labelOverlap(t1Labels map[string]int, t2 *treesitter.ASTNode) int { count := 0 diff --git a/internal/treesitter/ast.go b/internal/treesitter/ast.go index 63397e2..7186a65 100644 --- a/internal/treesitter/ast.go +++ b/internal/treesitter/ast.go @@ -22,6 +22,7 @@ type ASTNode struct { HasError bool // True if parse tree contained any ERROR nodes ParseErrorCount int // Total count of ERROR nodes in parse tree IsKeyword bool // True if node is a keyword token + IsUnordered bool // True if children of this container node are order-insensitive // Hash is the combined hash of node type, label, and children. Hash uint64 @@ -186,6 +187,9 @@ func buildASTWithRules(n *gotreesitter.Node, src []byte, lang *gotreesitter.Lang if slices.Contains(rules.Keywords, nodeType) || (label != "" && slices.Contains(rules.Keywords, label)) { node.IsKeyword = true } + if slices.Contains(rules.Unordered, node.Type) { + node.IsUnordered = true + } } for i := 0; i < n.ChildCount(); i++ { diff --git a/internal/treesitter/go/rules.yml b/internal/treesitter/go/rules.yml index 7904293..3f7aa57 100644 --- a/internal/treesitter/go/rules.yml +++ b/internal/treesitter/go/rules.yml @@ -124,4 +124,8 @@ declarations: - const_declaration - var_declaration +unordered: + - interface_type + + diff --git a/internal/treesitter/java/rules.yml b/internal/treesitter/java/rules.yml index 5c5ae90..88e1db5 100644 --- a/internal/treesitter/java/rules.yml +++ b/internal/treesitter/java/rules.yml @@ -187,4 +187,8 @@ declarations: - local_variable_declaration - field_declaration +unordered: + - annotation + + diff --git a/internal/treesitter/javascript/rules.yml b/internal/treesitter/javascript/rules.yml index e0ad87b..6c0a6c3 100644 --- a/internal/treesitter/javascript/rules.yml +++ b/internal/treesitter/javascript/rules.yml @@ -173,4 +173,10 @@ declarations: - lexical_declaration - field_definition +unordered: + - object + - object_pattern + + + diff --git a/internal/treesitter/php/rules.yml b/internal/treesitter/php/rules.yml index e467954..28cfed1 100644 --- a/internal/treesitter/php/rules.yml +++ b/internal/treesitter/php/rules.yml @@ -171,4 +171,8 @@ declarations: - property_declaration - const_declaration +unordered: + - array_creation_expression + - attribute_group + - attribute_list diff --git a/internal/treesitter/python/rules.yml b/internal/treesitter/python/rules.yml index b5242ed..958540b 100644 --- a/internal/treesitter/python/rules.yml +++ b/internal/treesitter/python/rules.yml @@ -155,4 +155,9 @@ declarations: - function_definition - class_definition +unordered: + - dictionary + - set + + diff --git a/internal/treesitter/ruby/rules.yml b/internal/treesitter/ruby/rules.yml index fb6bb6d..dbf5ed0 100644 --- a/internal/treesitter/ruby/rules.yml +++ b/internal/treesitter/ruby/rules.yml @@ -155,3 +155,9 @@ declarations: - class - module +unordered: + - hash + - hash_pattern + + + diff --git a/internal/treesitter/rules.go b/internal/treesitter/rules.go index 8d06e6c..e88af78 100644 --- a/internal/treesitter/rules.go +++ b/internal/treesitter/rules.go @@ -16,6 +16,7 @@ type Rules struct { Scaffolding []string `yaml:"scaffolding"` Keywords []string `yaml:"keywords"` Declarations []string `yaml:"declarations"` + Unordered []string `yaml:"unordered"` } //go:embed */rules.yml diff --git a/internal/treesitter/rules_test.go b/internal/treesitter/rules_test.go index 4a059c7..8142a0a 100644 --- a/internal/treesitter/rules_test.go +++ b/internal/treesitter/rules_test.go @@ -182,3 +182,15 @@ func TestRulesAliased(t *testing.T) { }) } } + +func TestRulesUnordered(t *testing.T) { + r := &Rules{ + Unordered: []string{"object", "start_tag"}, + } + if len(r.Unordered) != 2 { + t.Fatalf("expected 2 unordered entries, got %d", len(r.Unordered)) + } + if r.Unordered[0] != "object" || r.Unordered[1] != "start_tag" { + t.Errorf("unexpected unordered entries: %v", r.Unordered) + } +} diff --git a/internal/treesitter/rust/rules.yml b/internal/treesitter/rust/rules.yml index 7f9b6cc..b8dd603 100644 --- a/internal/treesitter/rust/rules.yml +++ b/internal/treesitter/rust/rules.yml @@ -151,4 +151,8 @@ declarations: - const_item - static_item +unordered: + - use_declaration + + diff --git a/internal/treesitter/tsx/rules.yml b/internal/treesitter/tsx/rules.yml index cdbe392..43f7f48 100644 --- a/internal/treesitter/tsx/rules.yml +++ b/internal/treesitter/tsx/rules.yml @@ -189,4 +189,14 @@ declarations: - variable_declaration - lexical_declaration +unordered: + - object + - interface_body + - object_type + - jsx_opening_element + - jsx_self_closing_element + - object_pattern + + + diff --git a/internal/treesitter/typescript/rules.yml b/internal/treesitter/typescript/rules.yml index 60ce22f..1fdc444 100644 --- a/internal/treesitter/typescript/rules.yml +++ b/internal/treesitter/typescript/rules.yml @@ -175,4 +175,12 @@ declarations: - variable_declaration - lexical_declaration +unordered: + - object + - interface_body + - object_type + - object_pattern + + + diff --git a/tests/testdata/json_schemastore_hugo_theme_config/expected.json b/tests/testdata/json_schemastore_hugo_theme_config/expected.json index b6958f8..f25e605 100644 --- a/tests/testdata/json_schemastore_hugo_theme_config/expected.json +++ b/tests/testdata/json_schemastore_hugo_theme_config/expected.json @@ -110,41 +110,6 @@ "position": 2, "subtree": true }, - { - "action": "insert", - "node": { - "tree": "after", - "path": [ - 0, - 4, - 1, - 86, - 1, - 4, - 1, - 4 - ], - "type": "pair", - "start_byte": 145500, - "end_byte": 145827 - }, - "parent": { - "tree": "after", - "path": [ - 0, - 4, - 1, - 86, - 1, - 4, - 1 - ], - "type": "object", - "start_byte": 144702, - "end_byte": 145835 - }, - "position": 4 - }, { "action": "insert", "node": { @@ -245,44 +210,6 @@ "end_byte": 144672 } }, - { - "action": "insert", - "node": { - "tree": "after", - "path": [ - 0, - 4, - 1, - 86, - 1, - 4, - 1, - 4, - 0 - ], - "type": "string", - "label": "\"priority\"", - "start_byte": 145500, - "end_byte": 145510 - }, - "parent": { - "tree": "after", - "path": [ - 0, - 4, - 1, - 86, - 1, - 4, - 1, - 4 - ], - "type": "pair", - "start_byte": 145500, - "end_byte": 145827 - }, - "position": 0 - }, { "action": "insert", "node": { @@ -417,11 +344,11 @@ 1, 4, 1, - 0 + 2 ], "type": "pair", - "start_byte": 145524, - "end_byte": 145592 + "start_byte": 145632, + "end_byte": 145792 }, "parent": { "tree": "after", @@ -440,11 +367,10 @@ "start_byte": 145512, "end_byte": 145827 }, - "position": 0, - "subtree": true + "position": 2 }, { - "action": "move", + "action": "update", "node": { "tree": "before", "path": [ @@ -455,78 +381,19 @@ 1, 4, 1, - 3, + 2, 1, - 1 - ], - "type": "pair", - "start_byte": 145258, - "end_byte": 145274 - }, - "parent": { - "tree": "after", - "path": [ 0, - 4, - 1, - 86, - 1, - 4, - 1, - 4, 1 ], - "type": "object", - "start_byte": 145512, - "end_byte": 145827 - }, - "position": 1, - "old_parent": { - "tree": "before", - "path": [ - 0, - 4, - 1, - 86, - 1, - 4, - 1, - 3, - 1 - ], - "type": "object", - "start_byte": 145149, - "end_byte": 145358 - }, - "old_position": 1, - "subtree": true, - "dest_start_byte": 145604, - "dest_end_byte": 145620 - }, - { - "action": "delete", - "node": { - "tree": "before", - "path": [ - 0, - 4, - 1, - 86, - 1, - 4, - 1, - 3, - 0 - ], "type": "string", - "label": "\"priority\"", - "start_byte": 145137, - "end_byte": 145147 - } - }, - { - "action": "insert", - "node": { + "label": "\"\\nhttps://gohugo.io/templates/sitemap-template/#configure-sitemapxml\"", + "start_byte": 144983, + "end_byte": 145053 + }, + "old_value": "\"\\nhttps://gohugo.io/templates/sitemap-template/#configure-sitemapxml\"", + "new_value": "\"\\nhttps://gohugo.io/configuration/sitemap/#filename\"", + "dest_node": { "tree": "after", "path": [ 0, @@ -536,33 +403,16 @@ 1, 4, 1, - 4, + 3, 1, - 3 - ], - "type": "pair", - "start_byte": 145804, - "end_byte": 145817 - }, - "parent": { - "tree": "after", - "path": [ 0, - 4, - 1, - 86, - 1, - 4, - 1, - 4, 1 ], - "type": "object", - "start_byte": 145512, - "end_byte": 145827 - }, - "position": 3, - "subtree": true + "type": "string", + "label": "\"\\nhttps://gohugo.io/configuration/sitemap/#filename\"", + "start_byte": 145363, + "end_byte": 145416 + } }, { "action": "update", @@ -576,18 +426,18 @@ 1, 4, 1, - 2, + 3, 1, 0, 1 ], "type": "string", "label": "\"\\nhttps://gohugo.io/templates/sitemap-template/#configure-sitemapxml\"", - "start_byte": 144983, - "end_byte": 145053 + "start_byte": 145176, + "end_byte": 145246 }, "old_value": "\"\\nhttps://gohugo.io/templates/sitemap-template/#configure-sitemapxml\"", - "new_value": "\"\\nhttps://gohugo.io/configuration/sitemap/#filename\"", + "new_value": "\"\\nhttps://gohugo.io/configuration/sitemap/#priority\"", "dest_node": { "tree": "after", "path": [ @@ -598,15 +448,15 @@ 1, 4, 1, - 3, + 4, 1, 0, 1 ], "type": "string", - "label": "\"\\nhttps://gohugo.io/configuration/sitemap/#filename\"", - "start_byte": 145363, - "end_byte": 145416 + "label": "\"\\nhttps://gohugo.io/configuration/sitemap/#priority\"", + "start_byte": 145539, + "end_byte": 145592 } }, { @@ -665,12 +515,13 @@ 1, 4, 1, - 2, + 3, 1 ], - "type": "array", - "start_byte": 145641, - "end_byte": 145792 + "type": "number", + "label": "-1", + "start_byte": 145815, + "end_byte": 145817 }, "parent": { "tree": "after", @@ -684,11 +535,11 @@ 1, 4, 1, - 2 + 3 ], "type": "pair", - "start_byte": 145632, - "end_byte": 145792 + "start_byte": 145804, + "end_byte": 145817 }, "position": 1 }, @@ -976,35 +827,14 @@ 1, 3, 1, - 0 - ], - "type": "pair", - "start_byte": 145161, - "end_byte": 145246 - }, - "subtree": true - }, - { - "action": "delete", - "node": { - "tree": "before", - "path": [ - 0, - 4, - 1, - 86, - 1, - 4, - 1, - 3, - 1, - 2 + 2, + 1 ], - "type": "pair", - "start_byte": 145286, + "type": "number", + "label": "0.5", + "start_byte": 145297, "end_byte": 145300 - }, - "subtree": true + } } ], "line_alignment": [ @@ -16890,18 +16720,10 @@ }, { "left_line": 3953, - "right_line": -1 - }, - { - "left_line": 3954, - "right_line": -1 - }, - { - "left_line": 3955, "right_line": 3970 }, { - "left_line": -1, + "left_line": 3954, "right_line": 3971 }, { @@ -16925,11 +16747,11 @@ "right_line": 3976 }, { - "left_line": 3956, + "left_line": -1, "right_line": 3977 }, { - "left_line": 3957, + "left_line": -1, "right_line": 3978 }, { @@ -16941,9 +16763,17 @@ "right_line": 3980 }, { - "left_line": -1, + "left_line": 3955, "right_line": 3981 }, + { + "left_line": 3956, + "right_line": -1 + }, + { + "left_line": 3957, + "right_line": -1 + }, { "left_line": 3958, "right_line": 3982 diff --git a/tests/testdata/py_requests_environment_bundle/expected.json b/tests/testdata/py_requests_environment_bundle/expected.json index 6a6ba8c..0cd5647 100644 --- a/tests/testdata/py_requests_environment_bundle/expected.json +++ b/tests/testdata/py_requests_environment_bundle/expected.json @@ -1,57 +1,6 @@ { "version": "v1", "actions": [ - { - "action": "move", - "node": { - "tree": "before", - "path": [ - 26, - 3, - 15, - 3, - 8, - 1, - 0 - ], - "type": "pair", - "start_byte": 28156, - "end_byte": 28172 - }, - "parent": { - "tree": "after", - "path": [ - 26, - 3, - 15, - 3, - 8, - 1 - ], - "type": "dictionary", - "start_byte": 28217, - "end_byte": 28345 - }, - "position": 2, - "old_parent": { - "tree": "before", - "path": [ - 26, - 3, - 15, - 3, - 8, - 1 - ], - "type": "dictionary", - "start_byte": 28155, - "end_byte": 28241 - }, - "old_position": 0, - "subtree": true, - "dest_start_byte": 28293, - "dest_end_byte": 28309 - }, { "action": "insert", "node": {