Skip to content

Commit 13ab62a

Browse files
Go: fix parsing of condition-only for loops using semicolons (#7560)
1 parent 5da0a22 commit 13ab62a

2 files changed

Lines changed: 88 additions & 7 deletions

File tree

rewrite-go/pkg/parser/go_parser.go

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1390,10 +1390,13 @@ func (ctx *parseContext) mapForStmt(stmt *ast.ForStmt) *tree.ForLoop {
13901390
// Go's AST normalizes `for ; cond; {}` to Init=nil, Post=nil, same as `for cond {}`.
13911391
// We detect semicolons by looking at the source text between for keyword and body.
13921392
is3Clause := stmt.Init != nil || stmt.Post != nil
1393+
bodyStart := int(stmt.Body.Lbrace) - ctx.file.Base()
13931394
if !is3Clause {
1394-
// Check for semicolons in the source between cursor and the body brace
1395-
bodyStart := int(stmt.Body.Lbrace) - ctx.file.Base()
1396-
if ctx.findNextBefore(';', bodyStart) >= 0 {
1395+
// `for ; cond; {}` has no Init/Post in the AST but is still
1396+
// syntactically 3-clause. Detect by scanning for `;` in the
1397+
// header — but skip rune/string literals so a `';'` inside the
1398+
// condition (e.g. `for tok != ';'`) isn't mistaken for one.
1399+
if ctx.findNextPositionOf(';', bodyStart) >= 0 {
13971400
is3Clause = true
13981401
}
13991402
}
@@ -1402,7 +1405,7 @@ func (ctx *parseContext) mapForStmt(stmt *ast.ForStmt) *tree.ForLoop {
14021405
// 3-clause for: for [init]; [cond]; [post] {}
14031406
if stmt.Init != nil {
14041407
init := ctx.mapStmt(stmt.Init)
1405-
semicolonOffset := ctx.findNext(';')
1408+
semicolonOffset := ctx.findNextPositionOf(';', bodyStart)
14061409
var after tree.Space
14071410
if semicolonOffset >= 0 {
14081411
after = ctx.prefix(ctx.file.Pos(semicolonOffset))
@@ -1412,7 +1415,7 @@ func (ctx *parseContext) mapForStmt(stmt *ast.ForStmt) *tree.ForLoop {
14121415
control.Init = &initRP
14131416
} else {
14141417
// No init but semicolons present: `for ; cond; post {}`
1415-
semicolonOffset := ctx.findNext(';')
1418+
semicolonOffset := ctx.findNextPositionOf(';', bodyStart)
14161419
var after tree.Space
14171420
if semicolonOffset >= 0 {
14181421
after = ctx.prefix(ctx.file.Pos(semicolonOffset))
@@ -1424,7 +1427,7 @@ func (ctx *parseContext) mapForStmt(stmt *ast.ForStmt) *tree.ForLoop {
14241427

14251428
if stmt.Cond != nil {
14261429
cond := ctx.mapExpr(stmt.Cond)
1427-
semicolonOffset := ctx.findNext(';')
1430+
semicolonOffset := ctx.findNextPositionOf(';', bodyStart)
14281431
after := tree.EmptySpace
14291432
if semicolonOffset >= 0 {
14301433
after = ctx.prefix(ctx.file.Pos(semicolonOffset))
@@ -1433,7 +1436,7 @@ func (ctx *parseContext) mapForStmt(stmt *ast.ForStmt) *tree.ForLoop {
14331436
condRP := tree.RightPadded[tree.Expression]{Element: cond, After: after}
14341437
control.Condition = &condRP
14351438
} else {
1436-
semicolonOffset := ctx.findNext(';')
1439+
semicolonOffset := ctx.findNextPositionOf(';', bodyStart)
14371440
after := tree.EmptySpace
14381441
if semicolonOffset >= 0 {
14391442
after = ctx.prefix(ctx.file.Pos(semicolonOffset))
@@ -2877,6 +2880,65 @@ func (ctx *parseContext) findNextBefore(ch byte, before int) int {
28772880
return -1
28782881
}
28792882

2883+
// findNextPositionOf is like findNextBefore but skips over Go rune
2884+
// literals ('...'), interpreted string literals ("..."), raw string literals
2885+
// (`...`), and `//` / `/* */` comments while scanning. A `before` of 0 means
2886+
// scan to end of src. Used for syntactic markers like `;` in a `for` header
2887+
// that can otherwise hide inside a `';'` rune literal or a `/* ; */` comment.
2888+
func (ctx *parseContext) findNextPositionOf(ch byte, before int) int {
2889+
end := len(ctx.src)
2890+
if before > 0 && before < end {
2891+
end = before
2892+
}
2893+
i := ctx.cursor
2894+
for i < end {
2895+
b := ctx.src[i]
2896+
switch {
2897+
case b == '\'' || b == '"':
2898+
quote := b
2899+
i++
2900+
for i < end {
2901+
c := ctx.src[i]
2902+
if c == '\\' && i+1 < end {
2903+
i += 2
2904+
continue
2905+
}
2906+
i++
2907+
if c == quote {
2908+
break
2909+
}
2910+
}
2911+
case b == '`':
2912+
i++
2913+
for i < end && ctx.src[i] != '`' {
2914+
i++
2915+
}
2916+
if i < end {
2917+
i++
2918+
}
2919+
case b == '/' && i+1 < end && ctx.src[i+1] == '/':
2920+
i += 2
2921+
for i < end && ctx.src[i] != '\n' {
2922+
i++
2923+
}
2924+
case b == '/' && i+1 < end && ctx.src[i+1] == '*':
2925+
i += 2
2926+
for i+1 < end && !(ctx.src[i] == '*' && ctx.src[i+1] == '/') {
2927+
i++
2928+
}
2929+
if i+1 < end {
2930+
i += 2
2931+
}
2932+
default:
2933+
if b == ch {
2934+
return i
2935+
}
2936+
i++
2937+
}
2938+
}
2939+
return -1
2940+
}
2941+
28802942
// findNextString finds the next occurrence of s from the current cursor.
28812943
func (ctx *parseContext) findNextString(s string) int {
28822944
idx := strings.Index(string(ctx.src[ctx.cursor:]), s)

rewrite-go/test/for_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,22 @@ func TestParseForRangeWithKeyValue(t *testing.T) {
6969
}
7070
`))
7171
}
72+
73+
func TestParseForCondWithSemicolonRuneLiteral(t *testing.T) {
74+
NewRecipeSpec().RewriteRun(t,
75+
Golang(`
76+
package main
77+
78+
func f(tok rune) {
79+
for tok != ';' {
80+
if true {
81+
continue
82+
}
83+
_ = ';'
84+
if true {
85+
} else {
86+
}
87+
}
88+
}
89+
`))
90+
}

0 commit comments

Comments
 (0)