Skip to content

Commit a91b3ee

Browse files
committed
Clean template and update diff tool
1 parent 08bce31 commit a91b3ee

4 files changed

Lines changed: 242 additions & 288 deletions

File tree

cmd/action/zapdiff.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,13 @@ type ZAPDiff struct {
2727
SpecLabel string `name:"spec-label" help:"Label for Spec (used in human-readable reports)" default:"Spec"`
2828
GenAttributes string `name:"gen-attributes" help:"Zap generation attributes"`
2929
MismatchLevel int `name:"mismatch-level" help:"Mismatch level to report (1-3); 1 is most important" default:"3"`
30+
SpecRoot string `name:"spec-root" help:"Path to Spec root directory" default:"."`
3031
}
3132

3233
func (z *ZAPDiff) Run(cc *cli.Context) (err error) {
3334
slog.Info("Running ZAP generation", "attributes", z.GenAttributes)
3435
zapCmd := &cli.ZAP{}
35-
zapCmd.Root = "."
36+
zapCmd.Root = z.SpecRoot
3637
zapCmd.SdkRoot = z.GeneratedSDKRoot
3738
zapCmd.Attribute = []string{z.GenAttributes}
3839
zapCmd.FeatureXML = true

zapdiff/diff.go

Lines changed: 134 additions & 183 deletions
Original file line numberDiff line numberDiff line change
@@ -4,240 +4,191 @@ import (
44
"bufio"
55
"fmt"
66
"os"
7-
"regexp"
87
"strings"
98
)
109

1110
type xpathSegment struct {
1211
tag string
1312
attr string
1413
value string
15-
isAttr bool // true for [@attr='val'], false for [tag='val']
14+
isAttr bool
1615
}
1716

18-
var segmentRegex = regexp.MustCompile(`^([a-zA-Z0-9_-]+)(?:\[(@?)([a-zA-Z0-9_()-]+)='([^']*)'\])?$`) // This regex is generated by Gemini
19-
2017
func parseEntityUniqueIdentifier(id string) ([]xpathSegment, error) {
2118
parts := strings.Split(id, "/")
2219
var segments []xpathSegment
23-
for _, part := range parts {
24-
if part == "" {
20+
for _, p := range parts {
21+
seg := xpathSegment{}
22+
bracketIdx := strings.Index(p, "[")
23+
if bracketIdx == -1 {
24+
seg.tag = p
25+
segments = append(segments, seg)
2526
continue
2627
}
27-
matches := segmentRegex.FindStringSubmatch(part)
28-
if matches == nil {
29-
return nil, fmt.Errorf("invalid segment: %s", part)
28+
seg.tag = p[:bracketIdx]
29+
rest := p[bracketIdx+1 : len(p)-1] // remove matching ]
30+
equalsIdx := strings.Index(rest, "=")
31+
if equalsIdx == -1 {
32+
return nil, fmt.Errorf("invalid segment: %s", p)
3033
}
31-
seg := xpathSegment{tag: matches[1]}
32-
if len(matches) > 3 && matches[3] != "" {
33-
seg.isAttr = matches[2] == "@"
34-
seg.attr = matches[3]
35-
seg.value = matches[4]
34+
seg.attr = rest[:equalsIdx]
35+
seg.value = strings.Trim(rest[equalsIdx+1:], "'")
36+
if strings.HasPrefix(seg.attr, "@") {
37+
seg.attr = seg.attr[1:]
38+
seg.isAttr = true
3639
}
3740
segments = append(segments, seg)
3841
}
3942
return segments, nil
4043
}
4144

42-
// findElementLines searches for the element identified by id in the file at path and returns its lines.
43-
func findElementLines(path string, id string) ([]string, int, error) {
44-
segments, err := parseEntityUniqueIdentifier(id)
45-
if err != nil {
46-
return nil, 0, err
47-
}
48-
49-
f, err := os.Open(path)
45+
// getCustomDiffLines reads a file and returns lines around the target element.
46+
// It returns the lines, the start index of the target in the returned lines,
47+
// the end index of the target, and an error.
48+
func getCustomDiffLines(path string, targetID string) ([]string, int, int, error) {
49+
file, err := os.Open(path)
5050
if err != nil {
51-
return nil, 0, err
51+
return nil, 0, 0, err
5252
}
53-
defer f.Close()
53+
defer file.Close()
5454

55-
scanner := bufio.NewScanner(f)
56-
var allLines []string
55+
var lines []string
56+
scanner := bufio.NewScanner(file)
5757
for scanner.Scan() {
58-
allLines = append(allLines, scanner.Text()+"\n")
58+
lines = append(lines, scanner.Text())
5959
}
6060
if err := scanner.Err(); err != nil {
61-
return nil, 0, err
61+
return nil, 0, 0, err
6262
}
6363

64-
segIdx := 0
65-
foundParent := false
66-
parentStartLine := -1
67-
startLine := -1
68-
69-
for i := 0; i < len(allLines); i++ {
70-
line := allLines[i]
71-
if segIdx >= len(segments) {
72-
break
73-
}
74-
seg := segments[segIdx]
75-
76-
// Boundary check: if we are looking for a child segment, and we see the parent close tag, we fail!
77-
if segIdx > 0 {
78-
parentTag := segments[segIdx-1].tag
79-
if strings.Contains(line, "</"+parentTag+">") {
80-
return nil, 0, nil // Not found within parent boundary
81-
}
82-
}
83-
84-
if seg.attr == "text()" {
85-
// Text content filter
86-
if strings.Contains(line, "<"+seg.tag) && strings.Contains(line, seg.value) {
87-
segIdx++
88-
if segIdx >= len(segments) {
89-
startLine = i
64+
segments, err := parseEntityUniqueIdentifier(targetID)
65+
if err != nil {
66+
return nil, 0, 0, err
67+
}
68+
69+
currentLine := 0
70+
lastFoundLine := -1
71+
foundLastSegment := false
72+
73+
for i, seg := range segments {
74+
found := false
75+
for j := currentLine; j < len(lines); j++ {
76+
line := lines[j]
77+
78+
// Guard against crossing into a sibling of the parent element
79+
if i > 0 {
80+
parentSeg := segments[i-1]
81+
// If we see another instance of the parent tag, we've likely left its scope
82+
if strings.Contains(line, "<"+parentSeg.tag) {
83+
break
9084
}
91-
}
92-
} else if seg.attr != "" && !seg.isAttr {
93-
// Child element filter
94-
if !foundParent {
95-
if strings.Contains(line, "<"+seg.tag) {
96-
foundParent = true
97-
parentStartLine = i
85+
// If we see the closing tag of the parent, we've definitely left its scope
86+
if strings.Contains(line, "</"+parentSeg.tag+">") {
87+
break
9888
}
9989
}
100-
if foundParent {
101-
childRegex := fmt.Sprintf(`<%s(\s+[^>]*)?>%s</%s>`, regexp.QuoteMeta(seg.attr), regexp.QuoteMeta(seg.value), regexp.QuoteMeta(seg.attr))
102-
matched, _ := regexp.MatchString(childRegex, line)
103-
if matched {
104-
segIdx++
105-
foundParent = false
106-
if segIdx >= len(segments) {
107-
startLine = parentStartLine
90+
91+
if strings.Contains(line, seg.tag) {
92+
if seg.isAttr {
93+
attrPattern1 := fmt.Sprintf(`%s="%s"`, seg.attr, seg.value)
94+
attrPattern2 := fmt.Sprintf(`%s='%s'`, seg.attr, seg.value)
95+
if strings.Contains(line, attrPattern1) || strings.Contains(line, attrPattern2) {
96+
lastFoundLine = j
97+
currentLine = j + 1
98+
found = true
99+
if i == len(segments)-1 {
100+
foundLastSegment = true
101+
}
102+
break
108103
}
104+
continue // Attributes must be on the same line as the tag
109105
}
110-
}
111-
} else {
112-
// Normal or attr filter
113-
if strings.Contains(line, "<"+seg.tag) {
114-
if seg.attr == "" {
115-
segIdx++
116-
if segIdx >= len(segments) {
117-
startLine = i
106+
107+
if seg.value != "" {
108+
if strings.Contains(line, seg.value) {
109+
lastFoundLine = j
110+
currentLine = j + 1 // Search next segment after this line
111+
found = true
112+
if i == len(segments)-1 {
113+
foundLastSegment = true
114+
}
115+
break
118116
}
119-
} else {
120-
attrStr1 := fmt.Sprintf(`%s="%s"`, seg.attr, seg.value)
121-
attrStr2 := fmt.Sprintf(`%s='%s'`, seg.attr, seg.value)
122-
if strings.Contains(line, attrStr1) || strings.Contains(line, attrStr2) {
123-
segIdx++
124-
if segIdx >= len(segments) {
125-
startLine = i
117+
// Search up to 5 lines ahead for value
118+
foundValue := false
119+
for k := 1; k <= 5 && j+k < len(lines); k++ {
120+
if strings.Contains(lines[j+k], seg.value) {
121+
lastFoundLine = j // Keep tag line as the found line for context
122+
currentLine = j + k + 1
123+
found = true
124+
foundValue = true
125+
if i == len(segments)-1 {
126+
foundLastSegment = true
127+
}
128+
break
126129
}
127130
}
131+
if foundValue {
132+
break
133+
}
134+
}
135+
if seg.value == "" {
136+
lastFoundLine = j
137+
currentLine = j + 1
138+
found = true
139+
if i == len(segments)-1 {
140+
foundLastSegment = true
141+
}
142+
break
128143
}
129144
}
130145
}
131-
}
132-
133-
if segIdx < len(segments) || startLine == -1 {
134-
return nil, 0, nil // Not found
135-
}
136-
137-
targetTag := segments[len(segments)-1].tag
138-
139-
isSelfClosing := false
140-
endLine := startLine
141-
for i := startLine; i < len(allLines); i++ {
142-
if strings.Contains(allLines[i], "/>") {
143-
isSelfClosing = true
144-
endLine = i
145-
break
146+
if found {
147+
// Check for self-closing tag if there are more segments to find
148+
if i < len(segments)-1 {
149+
trimmedLine := strings.TrimSpace(lines[lastFoundLine])
150+
if strings.HasSuffix(trimmedLine, "/>") {
151+
// Parent self-closes, cannot have children!
152+
found = false
153+
}
154+
}
146155
}
147-
if strings.Contains(allLines[i], ">") {
156+
if !found {
157+
// If a middle segment is not found, we might fail to find the target.
148158
break
149159
}
150160
}
151161

152-
if isSelfClosing {
153-
return allLines[startLine : endLine+1], startLine + 1, nil
154-
}
162+
var targetIdx int
163+
highlight := true
155164

156-
closeTag := "</" + targetTag + ">"
157-
for i := startLine; i < len(allLines); i++ {
158-
if strings.Contains(allLines[i], closeTag) {
159-
endLine = i
160-
break
161-
}
165+
if foundLastSegment {
166+
targetIdx = lastFoundLine
167+
} else if lastFoundLine != -1 {
168+
// Fallback to parent
169+
targetIdx = lastFoundLine
170+
highlight = false
171+
} else {
172+
// Element and all parents not found
173+
return nil, 0, 0, nil
162174
}
163175

164-
return allLines[startLine : endLine+1], startLine + 1, nil
165-
}
166-
167-
// GenerateUnifiedDiff generates a block-based unified diff between two sets of lines using LCS.
168-
func GenerateUnifiedDiff(lines1, lines2 []string, start1, start2 int) (string, error) {
169-
m := len(lines1)
170-
n := len(lines2)
171-
c := make([][]int, m+1)
172-
for i := range c {
173-
c[i] = make([]int, n+1)
176+
start := targetIdx - 8
177+
if start < 0 {
178+
start = 0
174179
}
175-
176-
for i := 1; i <= m; i++ {
177-
for j := 1; j <= n; j++ {
178-
if lines1[i-1] == lines2[j-1] {
179-
c[i][j] = c[i-1][j-1] + 1
180-
} else {
181-
c[i][j] = max(c[i-1][j], c[i][j-1])
182-
}
183-
}
180+
end := targetIdx + 8
181+
if end >= len(lines) {
182+
end = len(lines) - 1
184183
}
185184

186-
type edit struct {
187-
op rune // ' ', '-', '+'
188-
line string
185+
resultLines := lines[start : end+1]
186+
relStart := -1
187+
relEnd := -1
188+
if highlight {
189+
relStart = targetIdx - start
190+
relEnd = targetIdx - start
189191
}
190-
var edits []edit
191192

192-
var collectDiff func(i, j int)
193-
collectDiff = func(i, j int) {
194-
if i > 0 && j > 0 && lines1[i-1] == lines2[j-1] {
195-
collectDiff(i-1, j-1)
196-
edits = append(edits, edit{op: ' ', line: lines1[i-1]})
197-
} else if j > 0 && (i == 0 || c[i][j-1] >= c[i-1][j]) {
198-
collectDiff(i, j-1)
199-
edits = append(edits, edit{op: '+', line: lines2[j-1]})
200-
} else if i > 0 && (j == 0 || c[i][j-1] < c[i-1][j]) {
201-
collectDiff(i-1, j)
202-
edits = append(edits, edit{op: '-', line: lines1[i-1]})
203-
}
204-
}
205-
collectDiff(m, n)
206-
207-
var result strings.Builder
208-
result.WriteString("--- Ref\n")
209-
result.WriteString("+++ Generated\n")
210-
result.WriteString(fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", start1, m, start2, n))
211-
212-
idx := 0
213-
for idx < len(edits) {
214-
if edits[idx].op == ' ' {
215-
result.WriteString(" " + edits[idx].line)
216-
idx++
217-
} else {
218-
j := idx
219-
for j < len(edits) && edits[j].op != ' ' {
220-
j++
221-
}
222-
for k := idx; k < j; k++ {
223-
if edits[k].op == '-' {
224-
result.WriteString("-" + edits[k].line)
225-
}
226-
}
227-
for k := idx; k < j; k++ {
228-
if edits[k].op == '+' {
229-
result.WriteString("+" + edits[k].line)
230-
}
231-
}
232-
idx = j
233-
}
234-
}
235-
return result.String(), nil
236-
}
237-
238-
func max(a, b int) int {
239-
if a > b {
240-
return a
241-
}
242-
return b
193+
return resultLines, relStart, relEnd, nil
243194
}

0 commit comments

Comments
 (0)