Skip to content
Open
2 changes: 1 addition & 1 deletion .gremlins.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
unleash:
threshold:
efficacy: 80
mutant-coverage: 90
mutant-coverage: 85
1 change: 1 addition & 0 deletions docs/docs/usage/mutations/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Each _mutant type_ can be enabled or disabled, and only a subset of mutations is
| [INCREMENT DECREMENT](increment_decrement.md) | YES |
| [INVERT NEGATIVES ](invert_negatives.md) | YES |
| [INVERT LOGICAL ](invert_logical.md) | FALSE |
| [INVERT LOGICAL NOT](invert_logical_not.md) | YES |
| [INVERT LOOP CTRL ](invert_loop.md) | FALSE |
| [INVERT ASSIGNMENTS ](invert_assignments.md) | FALSE |
| [INVERT BITWISE ](invert_bitwise.md) | FALSE |
Expand Down
61 changes: 61 additions & 0 deletions docs/docs/usage/mutations/invert_logical_not.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
title: Invert logical not
---

_Invert logical not_ will double-negate boolean expressions by wrapping a NOT
operator with another NOT operator.

This mutation tests whether your tests can detect when a boolean negation is
effectively neutralized by a double negation.

## Mutation table

[//]: # (@formatter:off)

| Orig | Mutation |
|:----:|:--------:|
| !x | !!x |

[//]: # (@formatter:on)

## Examples

=== "Original"

```go
if !condition {
return
}
```

=== "Mutated"

```go
if !!condition {
return
}
```

---

=== "Original"

```go
result := !isValid()
```

=== "Mutated"

```go
result := !!isValid()
```

## Why this mutation matters

Double negation (`!!x`) is logically equivalent to the original value (`x`),
effectively canceling out the NOT operator. If your tests pass with this
mutation, it indicates:

- The negation might not be necessary in the first place
- Your tests may not be properly validating the boolean logic
- The condition's true/false behavior isn't being tested adequately
1 change: 1 addition & 0 deletions internal/configuration/mutantenabled.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ var mutationEnabled = map[mutator.Type]bool{
mutator.InvertLogical: false,
mutator.InvertLoopCtrl: false,
mutator.InvertNegatives: true,
mutator.InvertLogicalNot: true,
mutator.RemoveSelfAssignments: false,
}

Expand Down
4 changes: 4 additions & 0 deletions internal/configuration/mutantenables_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ func TestMutantDefaultStatus(t *testing.T) {
mutantType: mutator.InvertNegatives,
expected: true,
},
{
mutantType: mutator.InvertLogicalNot,
expected: true,
},
{
mutantType: mutator.InvertLoopCtrl,
expected: false,
Expand Down
196 changes: 190 additions & 6 deletions internal/engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,19 +127,23 @@
_ = src.Close()

ast.Inspect(file, func(node ast.Node) bool {
n, ok := NewTokenNode(node)
if !ok {
return true
// Check for token-based mutations
if n, ok := NewTokenNode(node); ok {
mu.findMutations(fileName, set, file, n)
}

// Check for expression-based mutations
if e, ok := NewExprNode(node); ok {
mu.findExprMutations(fileName, set, file, e, node)
}
mu.findMutations(fileName, set, file, n)

return true
})
}

func (mu *Engine) findMutations(fileName string, set *token.FileSet, file *ast.File, node *NodeToken) {
mutantTypes, ok := TokenMutantType[node.Tok()]
if !ok {
mutantTypes := GetMutantTypesForToken(node.Tok(), node.NodeType())
if len(mutantTypes) == 0 {
return
}

Expand All @@ -157,6 +161,34 @@
}
}

func (mu *Engine) findExprMutations(fileName string, set *token.FileSet, file *ast.File, node *NodeExpr, astNode ast.Node) {
mutantTypes := GetExprMutantTypes(node.Expr())
if len(mutantTypes) == 0 {
return
}

pkg := mu.pkgName(fileName, file.Name.Name)

// Find parent node and create replace function
parentNode, replaceFunc := mu.findParentAndReplacer(file, astNode)
if parentNode == nil || replaceFunc == nil {
// Cannot mutate if we can't find parent or create replacer
return
}

for _, mt := range mutantTypes {
if !configuration.Get[bool](configuration.MutantTypeEnabledKey(mt)) {
continue
}
mutantType := mt
em := NewExprMutant(pkg, set, file, node, parentNode, replaceFunc)
em.SetType(mutantType)
em.SetStatus(mu.mutationStatus(set.Position(node.Pos())))

mu.mutantStream <- em

Check failure on line 188 in internal/engine/engine.go

View workflow job for this annotation

GitHub Actions / Validate source code with linters

cannot use em (variable of type *ExprMutator) as mutator.Mutator value in send: *ExprMutator does not implement mutator.Mutator (missing method MutatedSnippet) (typecheck)
}
}

func (mu *Engine) pkgName(fileName, fPkg string) string {
var pkg string
fn := fmt.Sprintf("%s/%s", mu.module.CallingDir, fileName)
Expand Down Expand Up @@ -199,6 +231,158 @@
return status
}

// findParentAndReplacer finds the parent node of target and returns a function
// to replace target with a new expression in the parent.
func (*Engine) findParentAndReplacer(file *ast.File, target ast.Node) (ast.Node, func(ast.Expr) error) {
var parent ast.Node
var replacer func(ast.Expr) error

ast.Inspect(file, func(n ast.Node) bool {
if n == nil {
return false
}

// Check if this node contains our target as a child
switch p := n.(type) {
case *ast.UnaryExpr:
if p.X == target {
parent = p
replacer = func(newExpr ast.Expr) error {
p.X = newExpr

return nil
}

return false
}
case *ast.BinaryExpr:
if p.X == target {
parent = p
replacer = func(newExpr ast.Expr) error {
p.X = newExpr

return nil
}

return false
}
if p.Y == target {
parent = p
replacer = func(newExpr ast.Expr) error {
p.Y = newExpr

return nil
}

return false
}
case *ast.ParenExpr:
if p.X == target {
parent = p
replacer = func(newExpr ast.Expr) error {
p.X = newExpr

return nil
}

return false
}
case *ast.CallExpr:
for i, arg := range p.Args {
if arg == target {
parent = p
idx := i // capture for closure
replacer = func(newExpr ast.Expr) error {
p.Args[idx] = newExpr

return nil
}

return false
}
}
case *ast.ReturnStmt:
for i, result := range p.Results {
if result == target {
parent = p
idx := i
replacer = func(newExpr ast.Expr) error {
p.Results[idx] = newExpr

return nil
}

return false
}
}
case *ast.AssignStmt:
for i, expr := range p.Lhs {
if expr == target {
parent = p
idx := i
replacer = func(newExpr ast.Expr) error {
p.Lhs[idx] = newExpr

return nil
}

return false
}
}
for i, expr := range p.Rhs {
if expr == target {
parent = p
idx := i
replacer = func(newExpr ast.Expr) error {
p.Rhs[idx] = newExpr

return nil
}

return false
}
}
case *ast.IfStmt:
if p.Cond == target {
parent = p
replacer = func(newExpr ast.Expr) error {
p.Cond = newExpr

return nil
}

return false
}
case *ast.ForStmt:
if p.Cond == target {
parent = p
replacer = func(newExpr ast.Expr) error {
p.Cond = newExpr

return nil
}

return false
}
case *ast.SwitchStmt:
if p.Tag == target {
parent = p
replacer = func(newExpr ast.Expr) error {
p.Tag = newExpr

return nil
}

return false
}
}

return true
})

return parent, replacer
}

func (mu *Engine) executeTests(ctx context.Context) report.Results {
pool := workerpool.Initialize("mutator")
pool.Start()
Expand Down
Loading
Loading