diff --git a/libs/gorules/rule_benchmark_loop.go b/libs/gorules/rule_benchmark_loop.go new file mode 100644 index 00000000000..50eac3ab494 --- /dev/null +++ b/libs/gorules/rule_benchmark_loop.go @@ -0,0 +1,15 @@ +package gorules + +import "github.com/quasilyte/go-ruleguard/dsl" + +// UseBenchmarkLoop detects classic benchmark loops over b.N and suggests +// b.Loop() (Go 1.24+; the inlining regression was fixed in 1.26 so there's +// no longer a reason to keep b.N-based loops). +func UseBenchmarkLoop(m dsl.Matcher) { + m.Match( + `for $_ := range $b.N`, + `for range $b.N`, + ). + Where(m["b"].Type.Is("*testing.B")). + Report(`Use 'for $b.Loop()' instead of looping over $b.N (Go 1.24+, performance-correct in 1.26+)`) +} diff --git a/libs/structs/structdiff/bench_test.go b/libs/structs/structdiff/bench_test.go index f0406047835..08b386416ef 100644 --- a/libs/structs/structdiff/bench_test.go +++ b/libs/structs/structdiff/bench_test.go @@ -17,15 +17,13 @@ func bench(b *testing.B, job1, job2 string) { total := 0 - b.ResetTimer() - for range b.N { + for b.Loop() { changes, err := GetStructDiff(&x, &y, nil) if err != nil { b.Fatalf("error: %s", err) } total += len(changes) } - b.StopTimer() b.Logf("Total: %d / %d", total, b.N) } diff --git a/libs/structs/structtag/jsontag_test.go b/libs/structs/structtag/jsontag_test.go index b45fd566ea1..810d66851b7 100644 --- a/libs/structs/structtag/jsontag_test.go +++ b/libs/structs/structtag/jsontag_test.go @@ -59,7 +59,7 @@ var benchTags = []JSONTag{ } func BenchmarkJSONTagName(b *testing.B) { - for range b.N { + for b.Loop() { for _, tag := range benchTags { tag.Name() } @@ -67,7 +67,7 @@ func BenchmarkJSONTagName(b *testing.B) { } func BenchmarkJSONTagOmitEmpty(b *testing.B) { - for range b.N { + for b.Loop() { for _, tag := range benchTags { tag.OmitEmpty() } diff --git a/libs/structs/structwalk/walktype_bench_test.go b/libs/structs/structwalk/walktype_bench_test.go index 5f4b05bf986..5947ba5ca47 100644 --- a/libs/structs/structwalk/walktype_bench_test.go +++ b/libs/structs/structwalk/walktype_bench_test.go @@ -21,8 +21,7 @@ func countFields(typ reflect.Type) (int, error) { func benchmarkWalkType(b *testing.B, tt reflect.Type) { total := 0 - b.ResetTimer() - for range b.N { + for b.Loop() { count, err := countFields(tt) if err != nil { b.Fatalf("WalkType failed: %v", err) @@ -30,7 +29,6 @@ func benchmarkWalkType(b *testing.B, tt reflect.Type) { total += count } - b.StopTimer() // Root now correctly includes embedded struct fields, so it has many more fields than JobSettings // (3,487 vs 533) because it contains JobSettings plus other resource types and config fields b.Logf("Counted fields in %s: %d (%d/%d)", tt, total/b.N, total, b.N)