Developing a Custom Rule
Each rule needs to implement the lint.Rule
interface:
type Rule interface {
Name() string
Apply(*File, Arguments) []Failure
}
The Arguments
type is an alias of the type []interface{}
. The arguments of the rule are passed from the configuration file.
Example
Let us develop a rule called ArgumentsLimitRule
which sets a restriction on the number of arguments that a function may accept. As we explained above, our rule needs to implement the Rule
interface:
package rule
import (
"fmt"
"go/ast"
"github.com/mgechev/revive/lint"
)
// ArgumentsLimitRule lints given else constructs.
type ArgumentsLimitRule struct{}
// Apply applies the rule to given file.
func (r *ArgumentsLimitRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
// ...
return failures
}
// Name returns the rule name.
func (r *ArgumentsLimitRule) Name() string {
return "argument-limit"
}
type lintArgsNum struct {
total int
onFailure func(lint.Failure)
}
func (w lintArgsNum) Visit(n ast.Node) ast.Visitor {
// ...
}
In the snippet above, we first import the packages fmt
, go/ast
and the lint
package from revive
. After that we define a struct called ArgumentsLimitRule
which has two methods Apply
and Name
. Apply
accepts a file and the rule's argument, and Name
simply returns the name of the rule.
Notice how we also have the declaration of the lintArgsNum
struct. We're going to use it to traverse the program's AST.
Now let us fill the blanks!
// ...
// Apply applies the rule to given file.
func (r *ArgumentsLimitRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
if len(arguments) != 1 {
panic(`invalid configuration for "argument-limit"`)
}
total, ok := arguments[0].(int64) // Alt. non panicking version
if !ok {
panic(`invalid value passed as argument number to the "argument-list" rule`)
}
var failures []lint.Failure
walker := lintArgsNum{
total: int(total),
onFailure: func(failure lint.Failure) {
failures = append(failures, failure)
},
}
ast.Walk(walker, file.AST)
return failures
}
// ...
In the example above we perform the following algorithm:
- We first validate the arguments. The rule should accept a single argument of type
int64
, specifying the total number of allowed arguments - After that, we define a slice of type
[]lint.Failure
which we'll fill during the AST traversal - As the next step, we create a new instance of the struct
lintArgsNum
. The struct has two fields:total
, which specifies the rule's arguments, andonFailure
. The struct's instance will invoke theonFailure
callback when it finds a new failure. Inside of this callback, we'll push the failure to the list of failures - Next, we start the AST traversal
- Finally, we return the slice of collected failures
That's pretty much everything in the rule's declaration!
Now let us explore the AST traversal algorithm:
type lintArgsNum struct {
total int
onFailure func(lint.Failure)
}
func (w lintArgsNum) Visit(n ast.Node) ast.Visitor {
node, ok := n.(*ast.FuncDecl)
if ok {
num := 0
for _, l := range node.Type.Params.List {
for range l.Names {
num++
}
}
if num > w.total {
w.onFailure(lint.Failure{
Confidence: 1,
Failure: fmt.Sprintf("maximum number of arguments per function exceeded; max %d but got %d", w.total, num),
Node: node.Type,
})
return w
}
}
return w
}
The lintArgsNum
struct implements the ast.Visitor
interface, which means that it has a method called Visit
which accepts an ast.Node
and returns an ast.Visitor
.
Inside of the Visit
method, we check if the current node is of type *ast.FuncDecl
, and if it is, we validate the number of arguments the given function accepts. If it accepts more than the value of the property total
, then we invoke the onFailure
callback with the failure object.
Conclusion
In this guide we saw how we can implement a new rule in the framework for static code analysis for Go, revive.
We first explained how revive passes arguments defiled in the config file to the rules. After that, we looked at the lint.Rule
interface and implemented it in the rule ArgumentsLimitRule
. In the rule, we traversed the AST using the lintArgsNum
visitor which collected all the warnings it found in the process.