mirror of
https://github.com/alibaba/open-code-review.git
synced 2026-07-09 17:28:58 +00:00
This change refactors the `resolveFromHunk` function by introducing several helper functions (`extractSideLines`, `matchConsecutive`) to improve readability and maintainability. The new implementation attempts to match against both new and old sides of the diff hunk, making comment resolution more robust.
204 lines
5.2 KiB
Go
204 lines
5.2 KiB
Go
package diff
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/open-code-review/open-code-review/internal/model"
|
|
)
|
|
|
|
// ResolveLineNumbers populates StartLine/EndLine on each comment by matching
|
|
// the ExistingCode against the corresponding file's diff hunks (primary), or
|
|
// falling back to scanning the full new-file content line-by-line.
|
|
func ResolveLineNumbers(comments []model.LlmComment, diffs []model.Diff) []model.LlmComment {
|
|
if len(comments) == 0 || len(diffs) == 0 {
|
|
return comments
|
|
}
|
|
|
|
// Build lookup: newPath -> *Diff
|
|
diffByPath := make(map[string]*model.Diff, len(diffs))
|
|
for i := range diffs {
|
|
d := &diffs[i]
|
|
if d.NewPath != "/dev/null" && d.NewPath != "" {
|
|
diffByPath[d.NewPath] = d
|
|
}
|
|
if d.OldPath != "/dev/null" && d.OldPath != "" {
|
|
diffByPath[d.OldPath] = d
|
|
}
|
|
}
|
|
|
|
result := make([]model.LlmComment, len(comments))
|
|
copy(result, comments)
|
|
|
|
for i := range result {
|
|
cm := &result[i]
|
|
if cm.StartLine > 0 || cm.EndLine > 0 {
|
|
continue
|
|
}
|
|
if cm.ExistingCode == "" {
|
|
continue
|
|
}
|
|
d, ok := diffByPath[cm.Path]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// Primary: try matching from deleted/context lines in diff hunks
|
|
if resolveFromHunk(d, cm) {
|
|
continue
|
|
}
|
|
|
|
// Fallback: scan the new file content for consecutive matches
|
|
resolveFromFileContent(d, cm)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// indexedLine pairs a normalized line with its absolute file line number.
|
|
type indexedLine struct {
|
|
lineNum int
|
|
content string
|
|
}
|
|
|
|
// resolveFromHunk tries to find startLine/endLine by matching ExistingCode
|
|
// against hunk lines. It tries the new-side first (context + added lines →
|
|
// new-file line numbers), then falls back to old-side (context + deleted →
|
|
// old-file line numbers).
|
|
func resolveFromHunk(d *model.Diff, cm *model.LlmComment) bool {
|
|
hunks := ParseHunks(d.Diff)
|
|
if len(hunks) == 0 {
|
|
return false
|
|
}
|
|
|
|
targetLines := splitAndNormalize(cm.ExistingCode)
|
|
if len(targetLines) == 0 {
|
|
return false
|
|
}
|
|
|
|
for i := range hunks {
|
|
newSide := extractSideLines(&hunks[i], true)
|
|
if start, end, ok := matchConsecutive(newSide, targetLines); ok {
|
|
cm.StartLine = start
|
|
cm.EndLine = end
|
|
return true
|
|
}
|
|
}
|
|
|
|
for i := range hunks {
|
|
oldSide := extractSideLines(&hunks[i], false)
|
|
if start, end, ok := matchConsecutive(oldSide, targetLines); ok {
|
|
cm.StartLine = start
|
|
cm.EndLine = end
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// extractSideLines extracts one side of the diff from a hunk.
|
|
// When newSide is true, returns context+added lines with new-file line numbers.
|
|
// When newSide is false, returns context+deleted lines with old-file line numbers.
|
|
func extractSideLines(hunk *Hunk, newSide bool) []indexedLine {
|
|
var result []indexedLine
|
|
oldLine := hunk.OldStart
|
|
newLine := hunk.NewStart
|
|
|
|
for _, l := range hunk.Lines {
|
|
switch l.Type {
|
|
case HunkContext:
|
|
if newSide {
|
|
result = append(result, indexedLine{newLine, normalizeLine(l.Content)})
|
|
} else {
|
|
result = append(result, indexedLine{oldLine, normalizeLine(l.Content)})
|
|
}
|
|
oldLine++
|
|
newLine++
|
|
case HunkAdded:
|
|
if newSide {
|
|
result = append(result, indexedLine{newLine, normalizeLine(l.Content)})
|
|
}
|
|
newLine++
|
|
case HunkDeleted:
|
|
if !newSide {
|
|
result = append(result, indexedLine{oldLine, normalizeLine(l.Content)})
|
|
}
|
|
oldLine++
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// matchConsecutive scans sideLines for a consecutive run matching all targetLines.
|
|
func matchConsecutive(sideLines []indexedLine, targetLines []string) (startLine, endLine int, found bool) {
|
|
if len(targetLines) == 0 || len(sideLines) < len(targetLines) {
|
|
return 0, 0, false
|
|
}
|
|
for i := 0; i <= len(sideLines)-len(targetLines); i++ {
|
|
matched := true
|
|
for j, target := range targetLines {
|
|
if sideLines[i+j].content != target {
|
|
matched = false
|
|
break
|
|
}
|
|
}
|
|
if matched {
|
|
return sideLines[i].lineNum, sideLines[i+len(targetLines)-1].lineNum, true
|
|
}
|
|
}
|
|
return 0, 0, false
|
|
}
|
|
|
|
// resolveFromFileContent scans the new file content line-by-line for consecutive
|
|
// matches of the normalized existing_code. Ported from Java's findConsecutiveLines.
|
|
func resolveFromFileContent(d *model.Diff, cm *model.LlmComment) bool {
|
|
if d.NewFileContent == "" {
|
|
return false
|
|
}
|
|
|
|
fileLines := strings.Split(d.NewFileContent, "\n")
|
|
targetLines := splitAndNormalize(cm.ExistingCode)
|
|
if len(targetLines) == 0 || len(fileLines) < len(targetLines) {
|
|
return false
|
|
}
|
|
|
|
for i := 0; i <= len(fileLines)-len(targetLines); i++ {
|
|
matched := true
|
|
for j, target := range targetLines {
|
|
if normalizeLine(strings.TrimRight(fileLines[i+j], "\r")) != target {
|
|
matched = false
|
|
break
|
|
}
|
|
}
|
|
if matched {
|
|
cm.StartLine = i + 1
|
|
cm.EndLine = i + len(targetLines)
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// splitAndNormalize splits code text into lines and normalizes each one.
|
|
func splitAndNormalize(code string) []string {
|
|
raw := strings.Split(code, "\n")
|
|
result := make([]string, 0, len(raw))
|
|
for _, line := range raw {
|
|
n := normalizeLine(line)
|
|
if n == "" {
|
|
continue
|
|
}
|
|
result = append(result, n)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// normalizeLine removes leading/trailing whitespace and strips any leading
|
|
// '+' or '-' diff marker (mirrors Java's processTargetLineCode).
|
|
func normalizeLine(s string) string {
|
|
s = strings.TrimSpace(s)
|
|
s = strings.TrimPrefix(s, "+")
|
|
s = strings.TrimPrefix(s, "-")
|
|
return strings.TrimSpace(s)
|
|
}
|