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
14 changes: 14 additions & 0 deletions internal/actions/chawathe.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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)
Expand Down
43 changes: 43 additions & 0 deletions internal/actions/chawathe_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
27 changes: 26 additions & 1 deletion internal/engine/bottom-up.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions internal/treesitter/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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++ {
Expand Down
4 changes: 4 additions & 0 deletions internal/treesitter/go/rules.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,8 @@ declarations:
- const_declaration
- var_declaration

unordered:
- interface_type



4 changes: 4 additions & 0 deletions internal/treesitter/java/rules.yml
Original file line number Diff line number Diff line change
Expand Up @@ -187,4 +187,8 @@ declarations:
- local_variable_declaration
- field_declaration

unordered:
- annotation



6 changes: 6 additions & 0 deletions internal/treesitter/javascript/rules.yml
Original file line number Diff line number Diff line change
Expand Up @@ -173,4 +173,10 @@ declarations:
- lexical_declaration
- field_definition

unordered:
- object
- object_pattern




4 changes: 4 additions & 0 deletions internal/treesitter/php/rules.yml
Original file line number Diff line number Diff line change
Expand Up @@ -171,4 +171,8 @@ declarations:
- property_declaration
- const_declaration

unordered:
- array_creation_expression
- attribute_group
- attribute_list

5 changes: 5 additions & 0 deletions internal/treesitter/python/rules.yml
Original file line number Diff line number Diff line change
Expand Up @@ -155,4 +155,9 @@ declarations:
- function_definition
- class_definition

unordered:
- dictionary
- set



6 changes: 6 additions & 0 deletions internal/treesitter/ruby/rules.yml
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,9 @@ declarations:
- class
- module

unordered:
- hash
- hash_pattern



1 change: 1 addition & 0 deletions internal/treesitter/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions internal/treesitter/rules_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
4 changes: 4 additions & 0 deletions internal/treesitter/rust/rules.yml
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,8 @@ declarations:
- const_item
- static_item

unordered:
- use_declaration



10 changes: 10 additions & 0 deletions internal/treesitter/tsx/rules.yml
Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,14 @@ declarations:
- variable_declaration
- lexical_declaration

unordered:
- object
- interface_body
- object_type
- jsx_opening_element
- jsx_self_closing_element
- object_pattern




8 changes: 8 additions & 0 deletions internal/treesitter/typescript/rules.yml
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,12 @@ declarations:
- variable_declaration
- lexical_declaration

unordered:
- object
- interface_body
- object_type
- object_pattern




Loading
Loading