Skip to content

Commit 37b224d

Browse files
authored
refactor(updater): extract YAML parsing and fix replacement logic (#196)
1 parent f728594 commit 37b224d

File tree

1 file changed

+70
-30
lines changed

1 file changed

+70
-30
lines changed

internal/extensionmgr/updater.go

Lines changed: 70 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -76,55 +76,96 @@ func (u *DefaultConfigUpdater) AddExtension(path string, extension config.Extens
7676
// if the key was not present.
7777
func replaceYAMLSection(content, key, replacement string) (string, bool) {
7878
lines := strings.Split(content, "\n")
79-
prefix := key + ":"
8079

81-
startIdx := -1
80+
startIdx := findTopLevelKeyIndex(lines, key)
81+
if startIdx == -1 {
82+
return content, false
83+
}
84+
85+
endIdx := findSectionEnd(lines, startIdx)
86+
87+
return buildReplacedContent(lines, startIdx, endIdx, replacement), true
88+
}
89+
90+
// findTopLevelKeyIndex returns the line index of the first top-level YAML key
91+
// matching key, or -1 if not found. A top-level key has no leading whitespace
92+
// and is followed by a colon.
93+
func findTopLevelKeyIndex(lines []string, key string) int {
94+
prefix := key + ":"
8295
for i, line := range lines {
96+
if !isTopLevelLine(line) {
97+
continue
98+
}
8399
trimmed := strings.TrimSpace(line)
84-
// Match only top-level keys (no leading whitespace) that start with
85-
// the key name followed by a colon.
86-
if len(line) > 0 && line[0] != ' ' && line[0] != '\t' &&
87-
(trimmed == prefix || strings.HasPrefix(trimmed, prefix+" ") || strings.HasPrefix(trimmed, prefix+"\n")) {
88-
startIdx = i
89-
break
100+
if trimmed == prefix || strings.HasPrefix(trimmed, prefix+" ") || strings.HasPrefix(trimmed, prefix+"\n") {
101+
return i
90102
}
91103
}
104+
return -1
105+
}
92106

93-
if startIdx == -1 {
94-
return content, false
95-
}
107+
// isTopLevelLine reports whether a line is non-empty and has no leading
108+
// whitespace, indicating a top-level YAML key.
109+
func isTopLevelLine(line string) bool {
110+
return len(line) > 0 && line[0] != ' ' && line[0] != '\t'
111+
}
112+
113+
// isBlankLine reports whether a line is empty or contains only whitespace.
114+
func isBlankLine(line string) bool {
115+
return strings.TrimSpace(line) == ""
116+
}
96117

97-
// Determine the end of the section: all subsequent lines that are indented
98-
// or blank belong to this section. A non-indented, non-blank line marks
99-
// the start of the next section.
118+
// isIndentedLine reports whether a non-empty line starts with whitespace.
119+
func isIndentedLine(line string) bool {
120+
return len(line) > 0 && (line[0] == ' ' || line[0] == '\t')
121+
}
122+
123+
// findSectionEnd returns the index of the first line after the section that
124+
// starts at startIdx. Indented and blank lines belong to the section. A blank
125+
// line followed by a non-indented line (or EOF) marks the section boundary.
126+
func findSectionEnd(lines []string, startIdx int) int {
100127
endIdx := startIdx + 1
101128
for endIdx < len(lines) {
102129
line := lines[endIdx]
103-
if line == "" || strings.TrimSpace(line) == "" {
104-
// Blank lines might be part of the section or a separator.
105-
// Look ahead to see if the next non-blank line is still indented.
106-
ahead := endIdx + 1
107-
for ahead < len(lines) && strings.TrimSpace(lines[ahead]) == "" {
108-
ahead++
109-
}
110-
if ahead < len(lines) && len(lines[ahead]) > 0 && (lines[ahead][0] == ' ' || lines[ahead][0] == '\t') {
111-
// Still inside the section.
130+
if isBlankLine(line) {
131+
if ahead := skipBlankLines(lines, endIdx+1); isIndentedLine(safeLineAt(lines, ahead)) {
112132
endIdx = ahead + 1
113133
continue
114134
}
115-
// Blank line(s) followed by a top-level key or EOF: section ends here.
116135
break
117136
}
118-
if line[0] != ' ' && line[0] != '\t' {
119-
// Next top-level key: section ends before this line.
137+
if !isIndentedLine(line) {
120138
break
121139
}
122140
endIdx++
123141
}
142+
return endIdx
143+
}
144+
145+
// skipBlankLines advances from index start past any consecutive blank lines
146+
// and returns the index of the first non-blank line (or len(lines) if none).
147+
func skipBlankLines(lines []string, start int) int {
148+
i := start
149+
for i < len(lines) && isBlankLine(lines[i]) {
150+
i++
151+
}
152+
return i
153+
}
124154

125-
// Build the result: lines before the section + replacement + lines after.
155+
// safeLineAt returns lines[i] if i is within bounds, or an empty string
156+
// otherwise. This avoids index-out-of-range checks at call sites.
157+
func safeLineAt(lines []string, i int) string {
158+
if i < len(lines) {
159+
return lines[i]
160+
}
161+
return ""
162+
}
163+
164+
// buildReplacedContent assembles the final content by concatenating lines
165+
// before startIdx, the replacement text, and lines from endIdx onward.
166+
func buildReplacedContent(lines []string, startIdx, endIdx int, replacement string) string {
126167
var result strings.Builder
127-
for i := 0; i < startIdx; i++ {
168+
for i := range startIdx {
128169
result.WriteString(lines[i])
129170
result.WriteString("\n")
130171
}
@@ -136,6 +177,5 @@ func replaceYAMLSection(content, key, replacement string) (string, bool) {
136177
result.WriteString("\n")
137178
}
138179
}
139-
140-
return result.String(), true
180+
return result.String()
141181
}

0 commit comments

Comments
 (0)