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
56 changes: 28 additions & 28 deletions internal/postprocess/normalize.go
Original file line number Diff line number Diff line change
Expand Up @@ -415,11 +415,16 @@ func normalizeMovesByStructure(es *actions.EditScript, ms *engine.Mapping) *acti
// Pass 1: Find moves to demote and collect nodes to evict from the mapping.
// We have to do this first because Chawathe emits Update actions before Move,
// and we need to drop those orphaned updates in Pass 2.
toDemote := make(map[*treesitter.ASTNode]*treesitter.ASTNode)
demotedDescendants := make(map[*treesitter.ASTNode]struct{})
evicted := make(map[*treesitter.ASTNode]struct{})
for _, a := range es.Actions() {
if a.Type != actions.Move || a.Node == nil {
continue
}
if _, ok := demotedDescendants[a.Node]; ok {
continue
}
dstNode := a.DestNode
if dstNode == nil {
dstNode = ms.Src()[a.Node]
Expand All @@ -434,49 +439,44 @@ func normalizeMovesByStructure(es *actions.EditScript, ms *engine.Mapping) *acti
if !shouldDemoteMove(a.Node, dstNode, ms, r) {
continue
}
toDemote[a.Node] = dstNode
for _, d := range a.Node.Descendants() {
demotedDescendants[d] = struct{}{}
evicted[d] = struct{}{}
}
evicted[a.Node] = struct{}{}
}

// Pass 2: Rebuild the edit script, demoting flagged moves and dropping
// orphaned updates on evicted nodes.
// Pass 2: Rebuild the edit script: demote flagged moves to delete+insert,
// and drop any orphaned updates or nested moves inside those subtrees.
result := actions.NewEditScript()
for _, a := range es.Actions() {
// Suppress orphaned Update actions on nodes whose paired Move was demoted.
if a.Type == actions.Update && a.Node != nil {
if _, ok := evicted[a.Node]; ok {
continue
}
}

if a.Type != actions.Move || a.Node == nil {
result.Add(a)
continue
}

dstNode := a.DestNode
if dstNode == nil {
dstNode = ms.Src()[a.Node]
}
if dstNode == nil {
if a.Node == nil {
result.Add(a)
continue
}

r := rules.Get(a.Node.GetLanguage())
if r == nil {
switch a.Type {
case actions.Update:
// Drop updates on nodes that are getting deleted anyway.
if _, ok := evicted[a.Node]; ok {
continue
}
result.Add(a)
continue
}

if !shouldDemoteMove(a.Node, dstNode, ms, r) {
case actions.Move:
// Skip nested moves inside an ancestor that's already turned into a subtree delete+insert.
if _, ok := demotedDescendants[a.Node]; ok {
continue
}
dstNode, shouldDemote := toDemote[a.Node]
if !shouldDemote {
result.Add(a)
continue
}
demoteMoveToDelIns(result, ms, a.Node, dstNode)
default:
result.Add(a)
continue
}

demoteMoveToDelIns(result, ms, a.Node, dstNode)
}
return result
}
Expand Down
36 changes: 36 additions & 0 deletions internal/postprocess/normalize_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package postprocess

import (
"slices"
"testing"

"github.com/HarshK97/diffmantic/internal/actions"
Expand Down Expand Up @@ -1015,6 +1016,41 @@ func TestNormalizeMovesByStructure(t *testing.T) {
t.Errorf("expected non-move actions to pass through, got %d", result.Size())
}
})

t.Run("nested move inside demoted ancestor move is suppressed", func(t *testing.T) {
childSrc := mkNode("identifier", "val")
childSrc.Language = "go"
parentSrc := mkNode("expression_statement", "", childSrc)
parentSrc.Language = "go"
parentSrc.StartRow = 10
parentSrc.EndRow = 10

childDst := mkNode("identifier", "val")
childDst.Language = "go"
parentDst := mkNode("expression_statement", "", childDst)
parentDst.Language = "go"
parentDst.StartRow = 500
parentDst.EndRow = 500

msNested := engine.NewMapping()
msNested.Add(parentSrc, parentDst)
msNested.Add(childSrc, childDst)

es := actions.NewEditScript()
es.Add(actions.Action{Type: actions.Move, Node: parentSrc, DestNode: parentDst})
es.Add(actions.Action{Type: actions.Move, Node: childSrc, DestNode: childDst})

result := normalizeMovesByStructure(es, msNested)
if result.Size() != 2 {
t.Fatalf("expected exactly 2 actions (Delete+Insert) after ancestor demotion, got %d", result.Size())
}
if idx := slices.IndexFunc(result.Actions(), func(a actions.Action) bool {
return a.Type == actions.Move
}); idx != -1 {
a := result.Actions()[idx]
t.Fatalf("expected no move actions to survive demotion of ancestor move, got %s on %s", a.Type, a.Node.Type)
}
})
}

func TestSameScopeDeclaration(t *testing.T) {
Expand Down
Loading