diff_refiner.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. package hercules
  2. import (
  3. "unicode/utf8"
  4. "github.com/sergi/go-diff/diffmatchpatch"
  5. "gopkg.in/bblfsh/sdk.v1/uast"
  6. "gopkg.in/src-d/go-git.v4"
  7. )
  8. type FileDiffRefiner struct {
  9. }
  10. func (ref *FileDiffRefiner) Name() string {
  11. return "FileDiffRefiner"
  12. }
  13. func (ref *FileDiffRefiner) Provides() []string {
  14. arr := [...]string{DependencyFileDiff}
  15. return arr[:]
  16. }
  17. func (ref *FileDiffRefiner) Requires() []string {
  18. arr := [...]string{DependencyFileDiff, DependencyUastChanges}
  19. return arr[:]
  20. }
  21. func (ref *FileDiffRefiner) Features() []string {
  22. arr := [...]string{FeatureUast}
  23. return arr[:]
  24. }
  25. func (ref *FileDiffRefiner) ListConfigurationOptions() []ConfigurationOption {
  26. return []ConfigurationOption{}
  27. }
  28. func (ref *FileDiffRefiner) Configure(facts map[string]interface{}) {}
  29. func (ref *FileDiffRefiner) Initialize(repository *git.Repository) {
  30. }
  31. func (ref *FileDiffRefiner) Consume(deps map[string]interface{}) (map[string]interface{}, error) {
  32. changesList := deps[DependencyUastChanges].([]UASTChange)
  33. changes := map[string]UASTChange{}
  34. for _, change := range changesList {
  35. if change.Before != nil && change.After != nil {
  36. changes[change.Change.To.Name] = change
  37. }
  38. }
  39. diffs := deps[DependencyFileDiff].(map[string]FileDiffData)
  40. result := map[string]FileDiffData{}
  41. for fileName, oldDiff := range diffs {
  42. suspicious := map[int][2]int{}
  43. line := 0
  44. for i, diff := range oldDiff.Diffs {
  45. if i == len(oldDiff.Diffs)-1 {
  46. break
  47. }
  48. if diff.Type == diffmatchpatch.DiffInsert &&
  49. oldDiff.Diffs[i+1].Type == diffmatchpatch.DiffEqual {
  50. matched := 0
  51. runesAdded := []rune(diff.Text)
  52. runesEqual := []rune(oldDiff.Diffs[i+1].Text)
  53. for ; matched < len(runesAdded) && matched < len(runesEqual) &&
  54. runesAdded[matched] == runesEqual[matched]; matched++ {
  55. }
  56. if matched > 0 {
  57. suspicious[i] = [2]int{line, matched}
  58. }
  59. }
  60. if diff.Type != diffmatchpatch.DiffDelete {
  61. line += utf8.RuneCountInString(diff.Text)
  62. }
  63. }
  64. if len(suspicious) == 0 {
  65. result[fileName] = oldDiff
  66. continue
  67. }
  68. uastChange := changes[fileName]
  69. line2node := make([][]*uast.Node, oldDiff.NewLinesOfCode)
  70. visitEachNode(uastChange.After, func(node *uast.Node) {
  71. if node.StartPosition != nil && node.EndPosition != nil {
  72. for l := node.StartPosition.Line; l <= node.EndPosition.Line; l++ {
  73. nodes := line2node[l-1] // line starts with 1
  74. if nodes == nil {
  75. nodes = []*uast.Node{}
  76. }
  77. line2node[l-1] = append(nodes, node)
  78. }
  79. }
  80. })
  81. newDiff := FileDiffData{
  82. OldLinesOfCode: oldDiff.OldLinesOfCode,
  83. NewLinesOfCode: oldDiff.NewLinesOfCode,
  84. Diffs: []diffmatchpatch.Diff{},
  85. }
  86. skipNext := false
  87. for i, diff := range oldDiff.Diffs {
  88. if skipNext {
  89. skipNext = false
  90. continue
  91. }
  92. info, exists := suspicious[i]
  93. if !exists {
  94. newDiff.Diffs = append(newDiff.Diffs, diff)
  95. continue
  96. }
  97. line := info[0]
  98. matched := info[1]
  99. size := utf8.RuneCountInString(diff.Text)
  100. n1 := countNodesInInterval(line2node, line, line+size)
  101. n2 := countNodesInInterval(line2node, line+matched, line+size+matched)
  102. if n1 <= n2 {
  103. newDiff.Diffs = append(newDiff.Diffs, diff)
  104. continue
  105. }
  106. skipNext = true
  107. runes := []rune(diff.Text)
  108. newDiff.Diffs = append(newDiff.Diffs, diffmatchpatch.Diff{
  109. Type: diffmatchpatch.DiffEqual, Text: string(runes[:matched]),
  110. })
  111. newDiff.Diffs = append(newDiff.Diffs, diffmatchpatch.Diff{
  112. Type: diffmatchpatch.DiffInsert, Text: string(runes[matched:]) + string(runes[:matched]),
  113. })
  114. runes = []rune(oldDiff.Diffs[i+1].Text)
  115. if len(runes) > matched {
  116. newDiff.Diffs = append(newDiff.Diffs, diffmatchpatch.Diff{
  117. Type: diffmatchpatch.DiffEqual, Text: string(runes[matched:]),
  118. })
  119. }
  120. }
  121. result[fileName] = newDiff
  122. }
  123. return map[string]interface{}{DependencyFileDiff: result}, nil
  124. }
  125. // Depth first tree traversal.
  126. func visitEachNode(root *uast.Node, payload func(*uast.Node)) {
  127. queue := []*uast.Node{}
  128. queue = append(queue, root)
  129. for len(queue) > 0 {
  130. node := queue[len(queue)-1]
  131. queue = queue[:len(queue)-1]
  132. payload(node)
  133. for _, child := range node.Children {
  134. queue = append(queue, child)
  135. }
  136. }
  137. }
  138. func countNodesInInterval(occupiedMap [][]*uast.Node, start, end int) int {
  139. nodes := map[*uast.Node]bool{}
  140. for i := start; i < end; i++ {
  141. for _, node := range occupiedMap[i] {
  142. nodes[node] = true
  143. }
  144. }
  145. return len(nodes)
  146. }
  147. func init() {
  148. Registry.Register(&FileDiffRefiner{})
  149. }