Go tools provides a package go/analysis for analyzing go code.
The goal of this package as per it's documentation is :
A static analysis is a function that inspects a package of Go code and reports a set of diagnostics (typically mistakes in the code), and perhaps produces other results as well, such as suggested refactorings or other facts. An analysis that reports mistakes is informally called a "checker". For example, the printf checker reports mistakes in fmt.Printf format strings.
We will use this package to write a static analyzer of our own use case.
Use case that we are planning to do diagnostics of go code is to check if there are any unused Interface Types in the struct fields.
Example:
type Animal interface{
}
type Abstract interface{
}
type Test struct {
err error
abc Animal
}
End Goal:
Here the analyzer has to throw diagnostic error saying Abstract is unused.
To start with we will construct an analyzer with the fields as below:
var UnusedInterfaceAnalyzer = &analysis.Analyzer{
Name: "checkUnusedAnalyzer",
Doc: unusedIntDoc,
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: customrun,
}
Name is userdefined Name
Documentation helps to know what this analyzer checks for
For more details on this, check analyzer
we now have to write the analyzer logic in the function customrun.
So defining the function
func customrun(pass *analysis.Pass) (interface{}, error) {}
It takes pass as input, from the documentation pass, it does single unit of work. Here we have to write logic to check for unused interfaces.
In the pass struct we use ResultOf field. When driver runs the analyzers and make their results available in map which holds the results computed by the analyzers.
we will be using inspect package which provides an AST inspector for the syntax trees of a package.
func customrun(pass *analysis.Pass) (interface{}, error) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
inspect.Preorder(nil, func(n ast.Node) {
}
return nil, nil
}
the first parameter in preorder is used to filter out different types of ast nodes. Here we need TypeSpec , Interface Type and StructType.
defining a variable to filter ast.Nodes and passing this as first argument of preOrder function.
nodeFilter := []ast.Node{
(*ast.InterfaceType)(nil),
(*ast.TypeSpec)(nil),
(*ast.StructType)(nil),
}
this filter is sent as input to the inspector so that the preorder logic gets exeucted only for these Node types.
Now in the preorder logic, I am first storing all the interface Types into a map.
intfMaps := make(map[string]int)
inspect.Preorder(nodeFilter, func(n ast.Node) {
switch t := n.(type){
case *ast.TypeSpec:
// which are public
if t.Name.IsExported() {
switch t.Type.(type) {
// and are interfaces
case *ast.InterfaceType:
intfMaps[t.Name.Name] = 1
}
}
Now for the struct types, if we find any field that is using Interface will delete the key in the map.
By the time we parse all the struct types, we will have map that are unused Interfaces.
Let's see the updated code now:
inspect.Preorder(nodeFilter, func(n ast.Node) {
switch t := n.(type){
case *ast.TypeSpec:
// which are public
if t.Name.IsExported() {
switch t.Type.(type) {
// and are interfaces
case *ast.InterfaceType:
intfMaps[t.Name.Name] = 1
}
}
case *ast.StructType:
for _, field := range t.Fields.List {
switch field.Type.(type){
case *ast.Ident:
identName := field.Type.(*ast.Ident).Name
//Remove interfaces which are used
for k := range intfMaps{
if identName == k{
delete(intfMaps, k)
}
}
}
}
}
})
For each struct type, we will loop through each field in the struct
For each field get the field Type and if it matches the map intfMaps , delete the key so that map gets updated with unused Interface types.
After this we now have to loop through the intfMaps and send diagnostic report if map is not empty.
inspect.Preorder(nil, func(n ast.Node) {
switch t := n.(type){
case *ast.StructType:
for x := range intfMaps {
pass.Reportf(t.Pos(), "unused Interface %s", x)
}
}
})
Here if map has keys , the analyzer will throw diagnostic report as below:
unexpected diagnostic: unused Interface Abstract.
Testing:
For testing this code analysis has analysistest package. We need to have test data package which has our go code for which we want to test the analyzer on.
The repo hierarchy is like this:
import (
"golang.org/x/tools/go/analysis/analysistest"
"testing"
)
func TestCtxArg(t *testing.T) {
analysistest.Run(t, analysistest.TestData(), Analyzer)
}
when I run go test.
the output is :
unexpected diagnostic: unused Interface Abstract.
Complete code is available at: