open-code-review/internal/diff/resolver.go
2026-05-20 22:03:52 +08:00

174 lines
4.3 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
}
// resolveFromHunk tries to find the startLine/endLine by matching ExistingCode
// against "from" side lines (context + deleted) in the diff hunks.
// Returns true on success (comments fields are mutated in place).
func resolveFromHunk(d *model.Diff, cm *model.LlmComment) bool {
// TODO: re-track with llm
hunks := ParseHunks(d.Diff)
if len(hunks) == 0 {
return false
}
targetLines := splitAndNormalize(cm.ExistingCode)
if len(targetLines) == 0 {
return false
}
for i := range hunks {
hunk := &hunks[i]
offset := 0 // tracks position within old-file range
lines := hunk.Lines
for j := 0; j < len(lines); j++ {
line := lines[j]
switch line.Type {
case HunkAdded:
// Added lines are TO-side only; don't affect old-file offset
continue
case HunkContext, HunkDeleted:
// Both are FROM-side candidates
if matchAt(lines, j, targetLines) {
startLine := hunk.OldStart + offset
endLine := startLine + len(targetLines) - 1
cm.StartLine = startLine
cm.EndLine = endLine
return true
}
offset++
}
}
}
return false
}
// matchAt checks whether targetLines[i] matches the normalized content of
// lines[startIndex+i] for all i, where each matched line must be a "from" side
// line (context or deleted).
func matchAt(lines []HunkLine, startIndex int, targetLines []string) bool {
for i, target := range targetLines {
idx := startIndex + i
if idx >= len(lines) {
return false
}
l := lines[idx]
// Must be a "from" side line
if l.Type != HunkContext && l.Type != HunkDeleted {
return false
}
if normalizeLine(l.Content) != target {
return false
}
}
return true
}
// 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)
}