diff_refiner.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  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. uastChange, exists := changes[fileName]
  43. if !exists {
  44. result[fileName] = oldDiff
  45. continue
  46. }
  47. suspicious := map[int][2]int{}
  48. line := 0
  49. for i, diff := range oldDiff.Diffs {
  50. if i == len(oldDiff.Diffs)-1 {
  51. break
  52. }
  53. if diff.Type == diffmatchpatch.DiffInsert &&
  54. oldDiff.Diffs[i+1].Type == diffmatchpatch.DiffEqual {
  55. matched := 0
  56. runesAdded := []rune(diff.Text)
  57. runesEqual := []rune(oldDiff.Diffs[i+1].Text)
  58. for ; matched < len(runesAdded) && matched < len(runesEqual) &&
  59. runesAdded[matched] == runesEqual[matched]; matched++ {
  60. }
  61. if matched > 0 {
  62. suspicious[i] = [2]int{line, matched}
  63. }
  64. }
  65. if diff.Type != diffmatchpatch.DiffDelete {
  66. line += utf8.RuneCountInString(diff.Text)
  67. }
  68. }
  69. if len(suspicious) == 0 {
  70. result[fileName] = oldDiff
  71. continue
  72. }
  73. line2node := make([][]*uast.Node, oldDiff.NewLinesOfCode)
  74. VisitEachNode(uastChange.After, func(node *uast.Node) {
  75. if node.StartPosition != nil && node.EndPosition != nil {
  76. for l := node.StartPosition.Line; l <= node.EndPosition.Line; l++ {
  77. nodes := line2node[l-1] // line starts with 1
  78. if nodes == nil {
  79. nodes = []*uast.Node{}
  80. }
  81. line2node[l-1] = append(nodes, node)
  82. }
  83. }
  84. })
  85. newDiff := FileDiffData{
  86. OldLinesOfCode: oldDiff.OldLinesOfCode,
  87. NewLinesOfCode: oldDiff.NewLinesOfCode,
  88. Diffs: []diffmatchpatch.Diff{},
  89. }
  90. skipNext := false
  91. for i, diff := range oldDiff.Diffs {
  92. if skipNext {
  93. skipNext = false
  94. continue
  95. }
  96. info, exists := suspicious[i]
  97. if !exists {
  98. newDiff.Diffs = append(newDiff.Diffs, diff)
  99. continue
  100. }
  101. line := info[0]
  102. matched := info[1]
  103. size := utf8.RuneCountInString(diff.Text)
  104. n1 := countNodesInInterval(line2node, line, line+size)
  105. n2 := countNodesInInterval(line2node, line+matched, line+size+matched)
  106. if n1 <= n2 {
  107. newDiff.Diffs = append(newDiff.Diffs, diff)
  108. continue
  109. }
  110. skipNext = true
  111. runes := []rune(diff.Text)
  112. newDiff.Diffs = append(newDiff.Diffs, diffmatchpatch.Diff{
  113. Type: diffmatchpatch.DiffEqual, Text: string(runes[:matched]),
  114. })
  115. newDiff.Diffs = append(newDiff.Diffs, diffmatchpatch.Diff{
  116. Type: diffmatchpatch.DiffInsert, Text: string(runes[matched:]) + string(runes[:matched]),
  117. })
  118. runes = []rune(oldDiff.Diffs[i+1].Text)
  119. if len(runes) > matched {
  120. newDiff.Diffs = append(newDiff.Diffs, diffmatchpatch.Diff{
  121. Type: diffmatchpatch.DiffEqual, Text: string(runes[matched:]),
  122. })
  123. }
  124. }
  125. result[fileName] = newDiff
  126. }
  127. return map[string]interface{}{DependencyFileDiff: result}, nil
  128. }
  129. // Depth first tree traversal.
  130. func VisitEachNode(root *uast.Node, payload func(*uast.Node)) {
  131. queue := []*uast.Node{}
  132. queue = append(queue, root)
  133. for len(queue) > 0 {
  134. node := queue[len(queue)-1]
  135. queue = queue[:len(queue)-1]
  136. payload(node)
  137. for _, child := range node.Children {
  138. queue = append(queue, child)
  139. }
  140. }
  141. }
  142. func countNodesInInterval(occupiedMap [][]*uast.Node, start, end int) int {
  143. nodes := map[*uast.Node]bool{}
  144. for i := start; i < end; i++ {
  145. for _, node := range occupiedMap[i] {
  146. nodes[node] = true
  147. }
  148. }
  149. return len(nodes)
  150. }
  151. func init() {
  152. Registry.Register(&FileDiffRefiner{})
  153. }