Repository: mgechev/revive (2026-03-16T12:02:49Z)

Issue:
use-waitgroup-go: support for range statements
**Is your feature request related to a problem? Please describe.**

Currently, the rule `use-waitgroup-go:` does not report changing to `wg.Go(...)` in the following code:

```go
	wg := sync.WaitGroup{}

	wg.Add(int(jobs))

	for range int(jobs) {
		go func() {
			defer wg.Done()
			doSomething()
		}()
	}

	wg.Wait()
```

**Describe the solution you'd like**

Update the `use-waitgroup-go` rule.

**Additional context**

It's from https://github.com/kubernetes-sigs/kueue/pull/9892


Discussion:

Patch:
diff --git a/RULES_DESCRIPTIONS.md b/RULES_DESCRIPTIONS.md
index f73ee6657..3a729f161 100644
--- a/RULES_DESCRIPTIONS.md
+++ b/RULES_DESCRIPTIONS.md
@@ -1759,6 +1759,47 @@ _Limitations_: The rule doesn't rely on type information but on variable names t
 This means the rule search for `wg` (the defacto standard name for wait groups);
 if the waitgroup variable is named differently than `wg` the rule will skip it.
 
+### Examples (use-waitgroup-go)
+
+Before (violation):
+
+```go
+import "sync"
+
+func concurrentProcess(jobs int) {
+	wg := sync.WaitGroup{}
+
+	wg.Add(jobs)
+
+	for range jobs {
+		go func() {
+			defer wg.Done()
+			// do something
+		}()
+	}
+
+	wg.Wait()
+}
+```
+
+After (fixed):
+
+```go
+import "sync"
+
+func concurrentProcess(jobs int) {
+	wg := sync.WaitGroup{}
+
+	for range jobs {
+		wg.Go(func() {
+			// do something
+		})
+	}
+
+	wg.Wait()
+}
+```
+
 _Configuration_: N/A
 
 ## useless-break
diff --git a/rule/use_waitgroup_go.go b/rule/use_waitgroup_go.go
index 6f04fd65c..47afc5381 100644
--- a/rule/use_waitgroup_go.go
+++ b/rule/use_waitgroup_go.go
@@ -98,15 +98,9 @@ func (w *lintUseWaitGroupGo) analyzeBlock(b *ast.BlockStmt) {
 		// we will iterate from the (i+1)-th statement up to the last statement of block.List
 		for i++; i < len(stmts); i++ {
 			stmt := stmts[i]
-			// looking for a go statement
-			goStmt, ok := stmt.(*ast.GoStmt)
-			if !ok {
-				continue // not a go statement
-			}
-
-			// here we found a the go statement
-			// now let's check is the go statement is applied to a function literal that contains a wg.Done
-			if !w.hasCallToWgDone(goStmt) {
+			// looking for a go statement (directly or inside a for/range loop)
+			goStmt := w.findGoStmtWithWgDone(stmt)
+			if goStmt == nil {
 				continue
 			}
 
@@ -122,6 +116,38 @@ func (w *lintUseWaitGroupGo) analyzeBlock(b *ast.BlockStmt) {
 	}
 }
 
+// findGoStmtWithWgDone returns the first go statement with wg.Done found in stmt,
+// searching directly and one level deep inside for/range loop bodies.
+// Returns nil if no such go statement is found.
+func (w *lintUseWaitGroupGo) findGoStmtWithWgDone(stmt ast.Stmt) *ast.GoStmt {
+	switch s := stmt.(type) {
+	case *ast.GoStmt:
+		if w.hasCallToWgDone(s) {
+			return s
+		}
+	case *ast.ForStmt:
+		return w.seekGoStmtWithWgDoneInBlock(s.Body)
+	case *ast.RangeStmt:
+		return w.seekGoStmtWithWgDoneInBlock(s.Body)
+	}
+	return nil
+}
+
+// seekGoStmtWithWgDoneInBlock returns the first go statement with wg.Done
+// found as a direct child of block, or nil if none is found.
+func (w *lintUseWaitGroupGo) seekGoStmtWithWgDoneInBlock(block *ast.BlockStmt) *ast.GoStmt {
+	if block == nil {
+		return nil
+	}
+	for _, s := range block.List {
+		goStmt, ok := s.(*ast.GoStmt)
+		if ok && w.hasCallToWgDone(goStmt) {
+			return goStmt
+		}
+	}
+	return nil
+}
+
 // hasCallToWgDone returns true if the given go statement
 // calls to a function literal containing a call to wg.Done, false otherwise.
 func (*lintUseWaitGroupGo) hasCallToWgDone(goStmt *ast.GoStmt) bool {


Test patch:
diff --git a/testdata/go1.25/use_waitgroup_go.go b/testdata/go1.25/use_waitgroup_go.go
index a827a49e9..3287d219b 100644
--- a/testdata/go1.25/use_waitgroup_go.go
+++ b/testdata/go1.25/use_waitgroup_go.go
@@ -51,3 +51,19 @@ func useWaitGroupGo() {
 	}()
 	wg.Wait()
 }
+
+// from https://github.com/kubernetes-sigs/kueue/blob/4f2d0d2ef10c634daa4766be5f308fe2cda7503a/cmd/importer/util/util.go#L260
+func concurrentJobs(jobs uint) {
+	wg := sync.WaitGroup{}
+
+	wg.Add(int(jobs)) // MATCH /replace wg.Add()...go {...wg.Done()...} with wg.Go(...)/
+
+	for range int(jobs) {
+		go func() {
+			defer wg.Done()
+			doSomething()
+		}()
+	}
+
+	wg.Wait()
+}