Skip to content

Commit a846e05

Browse files
committed
feat: add ErrorHandlingMutator for error return value mutations
Implement ErrorHandlingMutator that replaces error return values (err) with nil. Targets return statements containing an identifier named "err". Closes #8
1 parent 1f27a3a commit a846e05

File tree

5 files changed

+342
-8
lines changed

5 files changed

+342
-8
lines changed

internal/mutation/engine_test.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ func TestNew(t *testing.T) {
7474
t.Fatal("Expected engine to be non-nil")
7575
}
7676

77-
if len(engine.mutators) != 6 {
78-
t.Errorf("Expected 6 mutators, got %d", len(engine.mutators))
77+
if len(engine.mutators) != 7 {
78+
t.Errorf("Expected 7 mutators, got %d", len(engine.mutators))
7979
}
8080

8181
// Check mutator types
@@ -85,7 +85,7 @@ func TestNew(t *testing.T) {
8585
mutatorNames[mutator.Name()] = true
8686
}
8787

88-
expectedMutators := []string{"arithmetic", "branch", "conditional", "logical", "return"}
88+
expectedMutators := []string{"arithmetic", "branch", "conditional", "error_handling", "logical", "return"}
8989

9090
for _, expected := range expectedMutators {
9191
if !mutatorNames[expected] {
@@ -101,8 +101,8 @@ func TestNew_CustomMutators(t *testing.T) {
101101
t.Fatalf("Failed to create mutation engine: %v", err)
102102
}
103103

104-
if len(engine.mutators) != 6 {
105-
t.Errorf("Expected 6 mutators (all types enabled by default), got %d", len(engine.mutators))
104+
if len(engine.mutators) != 7 {
105+
t.Errorf("Expected 7 mutators (all types enabled by default), got %d", len(engine.mutators))
106106
}
107107
}
108108

@@ -114,8 +114,8 @@ func TestNew_InvalidMutator(t *testing.T) {
114114
}
115115

116116
// Should ignore invalid mutator
117-
if len(engine.mutators) != 6 {
118-
t.Errorf("Expected 6 mutators (all types enabled by default), got %d", len(engine.mutators))
117+
if len(engine.mutators) != 7 {
118+
t.Errorf("Expected 7 mutators (all types enabled by default), got %d", len(engine.mutators))
119119
}
120120
}
121121

@@ -737,6 +737,8 @@ func getExpectedMutatorName(mutantType string) string {
737737
return "branch"
738738
case strings.HasPrefix(mutantType, "conditional_"):
739739
return "conditional"
740+
case strings.HasPrefix(mutantType, "error_"):
741+
return "error_handling"
740742
case strings.HasPrefix(mutantType, "logical_"):
741743
return "logical"
742744
case strings.HasPrefix(mutantType, "bitwise_"):

internal/mutation/errorhandling.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package mutation
2+
3+
import (
4+
"fmt"
5+
"go/ast"
6+
"go/token"
7+
)
8+
9+
const (
10+
errorHandlingMutatorName = "error_handling"
11+
errorNilifyType = "error_nilify"
12+
)
13+
14+
// ErrorHandlingMutator mutates error return values by replacing err with nil.
15+
type ErrorHandlingMutator struct {
16+
}
17+
18+
// Name returns the name of the mutator.
19+
func (m *ErrorHandlingMutator) Name() string {
20+
return errorHandlingMutatorName
21+
}
22+
23+
// CanMutate returns true if the node is a return statement containing an err identifier.
24+
func (m *ErrorHandlingMutator) CanMutate(node ast.Node) bool {
25+
stmt, ok := node.(*ast.ReturnStmt)
26+
if !ok {
27+
return false
28+
}
29+
30+
for _, expr := range stmt.Results {
31+
if isErrIdent(expr) {
32+
return true
33+
}
34+
}
35+
36+
return false
37+
}
38+
39+
// Mutate generates mutants for the given node.
40+
func (m *ErrorHandlingMutator) Mutate(node ast.Node, fset *token.FileSet) []Mutant {
41+
stmt, ok := node.(*ast.ReturnStmt)
42+
if !ok {
43+
return nil
44+
}
45+
46+
var mutants []Mutant
47+
48+
for _, expr := range stmt.Results {
49+
ident, ok := expr.(*ast.Ident)
50+
if !ok || ident.Name != "err" {
51+
continue
52+
}
53+
54+
pos := fset.Position(ident.Pos())
55+
56+
mutants = append(mutants, Mutant{
57+
Line: pos.Line,
58+
Column: pos.Column,
59+
Type: errorNilifyType,
60+
Original: ident.Name,
61+
Mutated: "nil",
62+
Description: fmt.Sprintf("Replace return %s with return nil", ident.Name),
63+
})
64+
}
65+
66+
return mutants
67+
}
68+
69+
// Apply applies the mutation to the given AST node.
70+
func (m *ErrorHandlingMutator) Apply(node ast.Node, mutant Mutant) bool {
71+
stmt, ok := node.(*ast.ReturnStmt)
72+
if !ok {
73+
return false
74+
}
75+
76+
if mutant.Type != errorNilifyType {
77+
return false
78+
}
79+
80+
for i, expr := range stmt.Results {
81+
ident, ok := expr.(*ast.Ident)
82+
if !ok {
83+
continue
84+
}
85+
86+
if ident.Name != mutant.Original {
87+
continue
88+
}
89+
90+
stmt.Results[i] = &ast.Ident{Name: "nil"}
91+
92+
return true
93+
}
94+
95+
return false
96+
}
97+
98+
// isErrIdent reports whether expr is an identifier named "err".
99+
func isErrIdent(expr ast.Expr) bool {
100+
ident, ok := expr.(*ast.Ident)
101+
102+
return ok && ident.Name == "err"
103+
}
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
package mutation
2+
3+
import (
4+
"go/ast"
5+
"go/parser"
6+
"go/token"
7+
"testing"
8+
)
9+
10+
func TestErrorHandlingMutator_Name(t *testing.T) {
11+
t.Parallel()
12+
13+
mutator := &ErrorHandlingMutator{}
14+
15+
if mutator.Name() != errorHandlingMutatorName {
16+
t.Errorf("Name() = %q, want %q", mutator.Name(), errorHandlingMutatorName)
17+
}
18+
}
19+
20+
func TestErrorHandlingMutator_CanMutate(t *testing.T) {
21+
t.Parallel()
22+
23+
mutator := &ErrorHandlingMutator{}
24+
25+
tests := []struct {
26+
name string
27+
src string
28+
expected bool
29+
}{
30+
{
31+
name: "return with err and other value",
32+
src: "package main\nfunc f() (int, error) { err := error(nil); return 0, err }",
33+
expected: true,
34+
},
35+
{
36+
name: "return nil only",
37+
src: "package main\nfunc f() error { return nil }",
38+
expected: false,
39+
},
40+
{
41+
name: "return err alone",
42+
src: "package main\nfunc f() error { err := error(nil); return err }",
43+
expected: true,
44+
},
45+
{
46+
name: "return x and y without err",
47+
src: "package main\nfunc f() (int, int) { x, y := 1, 2; return x, y }",
48+
expected: false,
49+
},
50+
}
51+
52+
for _, tt := range tests {
53+
t.Run(tt.name, func(t *testing.T) {
54+
t.Parallel()
55+
56+
fset := token.NewFileSet()
57+
58+
file, err := parser.ParseFile(fset, "test.go", tt.src, 0)
59+
if err != nil {
60+
t.Fatalf("Failed to parse file: %v", err)
61+
}
62+
63+
var retStmt ast.Node
64+
65+
ast.Inspect(file, func(n ast.Node) bool {
66+
if rs, ok := n.(*ast.ReturnStmt); ok {
67+
retStmt = rs
68+
69+
return false
70+
}
71+
72+
return true
73+
})
74+
75+
if retStmt == nil {
76+
t.Fatalf("ReturnStmt not found in: %s", tt.src)
77+
}
78+
79+
if got := mutator.CanMutate(retStmt); got != tt.expected {
80+
t.Errorf("CanMutate() = %v, want %v", got, tt.expected)
81+
}
82+
})
83+
}
84+
}
85+
86+
func TestErrorHandlingMutator_CanMutate_NonReturnNode(t *testing.T) {
87+
t.Parallel()
88+
89+
mutator := &ErrorHandlingMutator{}
90+
91+
expr, err := parser.ParseExpr("a && b")
92+
if err != nil {
93+
t.Fatalf("Failed to parse expression: %v", err)
94+
}
95+
96+
if mutator.CanMutate(expr) {
97+
t.Error("CanMutate() = true, want false for non-ReturnStmt node")
98+
}
99+
}
100+
101+
func TestErrorHandlingMutator_Mutate(t *testing.T) {
102+
t.Parallel()
103+
104+
mutator := &ErrorHandlingMutator{}
105+
fset := token.NewFileSet()
106+
107+
src := "package main\nfunc f() (int, error) { err := error(nil); return 0, err }"
108+
109+
file, err := parser.ParseFile(fset, "test.go", src, 0)
110+
if err != nil {
111+
t.Fatalf("Failed to parse file: %v", err)
112+
}
113+
114+
var retStmt ast.Node
115+
116+
ast.Inspect(file, func(n ast.Node) bool {
117+
if rs, ok := n.(*ast.ReturnStmt); ok {
118+
retStmt = rs
119+
120+
return false
121+
}
122+
123+
return true
124+
})
125+
126+
if retStmt == nil {
127+
t.Fatal("ReturnStmt not found")
128+
}
129+
130+
mutants := mutator.Mutate(retStmt, fset)
131+
132+
if len(mutants) != 1 {
133+
t.Fatalf("Expected 1 mutant, got %d", len(mutants))
134+
}
135+
136+
m := mutants[0]
137+
138+
if m.Type != errorNilifyType {
139+
t.Errorf("Type = %q, want %q", m.Type, errorNilifyType)
140+
}
141+
142+
if m.Original != "err" {
143+
t.Errorf("Original = %q, want %q", m.Original, "err")
144+
}
145+
146+
if m.Mutated != "nil" {
147+
t.Errorf("Mutated = %q, want %q", m.Mutated, "nil")
148+
}
149+
150+
if m.Line <= 0 {
151+
t.Errorf("Expected positive line number, got %d", m.Line)
152+
}
153+
154+
if m.Description == "" {
155+
t.Error("Expected non-empty description")
156+
}
157+
}
158+
159+
func TestErrorHandlingMutator_Apply(t *testing.T) {
160+
t.Parallel()
161+
162+
mutator := &ErrorHandlingMutator{}
163+
fset := token.NewFileSet()
164+
165+
src := "package main\nfunc f() (int, error) { err := error(nil); return 0, err }"
166+
167+
file, err := parser.ParseFile(fset, "test.go", src, 0)
168+
if err != nil {
169+
t.Fatalf("Failed to parse file: %v", err)
170+
}
171+
172+
var retStmt *ast.ReturnStmt
173+
174+
ast.Inspect(file, func(n ast.Node) bool {
175+
if rs, ok := n.(*ast.ReturnStmt); ok {
176+
retStmt = rs
177+
178+
return false
179+
}
180+
181+
return true
182+
})
183+
184+
if retStmt == nil {
185+
t.Fatal("ReturnStmt not found")
186+
}
187+
188+
mutant := Mutant{
189+
Type: errorNilifyType,
190+
Original: "err",
191+
Mutated: "nil",
192+
}
193+
194+
if !mutator.Apply(retStmt, mutant) {
195+
t.Error("Apply() = false, want true")
196+
}
197+
198+
ident, ok := retStmt.Results[1].(*ast.Ident)
199+
if !ok {
200+
t.Fatal("Expected *ast.Ident in return results[1] after Apply")
201+
}
202+
203+
if ident.Name != "nil" {
204+
t.Errorf("ident.Name = %q, want %q", ident.Name, "nil")
205+
}
206+
}
207+
208+
func TestErrorHandlingMutator_Apply_NonReturnStmt(t *testing.T) {
209+
t.Parallel()
210+
211+
mutator := &ErrorHandlingMutator{}
212+
213+
expr, err := parser.ParseExpr("a && b")
214+
if err != nil {
215+
t.Fatalf("Failed to parse expression: %v", err)
216+
}
217+
218+
mutant := Mutant{
219+
Type: errorNilifyType,
220+
Original: "err",
221+
Mutated: "nil",
222+
}
223+
224+
if mutator.Apply(expr, mutant) {
225+
t.Error("Apply() = true, want false for non-ReturnStmt node")
226+
}
227+
}

internal/mutation/registry.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)