Compiler Plugin

Exercise: Compiler Plugin

Difficulty - Advanced

Learning Objectives

  • Implement go/analysis analyzer
  • Parse and inspect Go AST
  • Report code issues and diagnostics
  • Suggest and apply code fixes
  • Integrate with go vet and golangci-lint
  • Understand compiler phases

Problem Statement

Create a custom static analysis tool that detects common mistakes and suggests fixes.

Core Components

 1package analyzer
 2
 3import (
 4    "go/ast"
 5    "golang.org/x/tools/go/analysis"
 6)
 7
 8var Analyzer = &analysis.Analyzer{
 9    Name: "customcheck",
10    Doc:  "checks for common mistakes",
11    Run:  run,
12}
13
14func run(pass *analysis.Pass)
15func checkErrorHandling(pass *analysis.Pass, node ast.Node)
16func suggestFix(pass *analysis.Pass, node ast.Node, message string)

Solution

Click to see the solution
  1package analyzer
  2
  3import (
  4    "go/ast"
  5    "go/token"
  6    "go/types"
  7
  8    "golang.org/x/tools/go/analysis"
  9    "golang.org/x/tools/go/analysis/passes/inspect"
 10    "golang.org/x/tools/go/ast/inspector"
 11)
 12
 13var Analyzer = &analysis.Analyzer{
 14    Name:     "customcheck",
 15    Doc:      "reports common Go mistakes",
 16    Run:      run,
 17    Requires: []*analysis.Analyzer{inspect.Analyzer},
 18}
 19
 20func run(pass *analysis.Pass) {
 21    inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
 22
 23    nodeFilter := []ast.Node{
 24       (nil),
 25       (nil),
 26       (nil),
 27       (nil),
 28    }
 29
 30    inspect.Preorder(nodeFilter, func(n ast.Node) {
 31        switch node := n.(type) {
 32        case *ast.FuncDecl:
 33            checkFunctionComplexity(pass, node)
 34        case *ast.CallExpr:
 35            checkDeferInLoop(pass, node)
 36        case *ast.RangeStmt:
 37            checkRangeValue(pass, node)
 38        case *ast.AssignStmt:
 39            checkErrorShadowing(pass, node)
 40        }
 41    })
 42
 43    return nil, nil
 44}
 45
 46// Check 1: Function complexity
 47func checkFunctionComplexity(pass *analysis.Pass, fn *ast.FuncDecl) {
 48    if fn.Body == nil {
 49        return
 50    }
 51
 52    complexity := calculateComplexity(fn.Body)
 53    if complexity > 10 {
 54        pass.Reportf(fn.Pos(),
 55            "function %s has complexity %d",
 56            fn.Name.Name, complexity)
 57    }
 58}
 59
 60func calculateComplexity(node ast.Node) int {
 61    complexity := 1
 62    ast.Inspect(node, func(n ast.Node) bool {
 63        switch n.(type) {
 64        case *ast.IfStmt, *ast.ForStmt, *ast.RangeStmt,
 65            *ast.CaseClause, *ast.CommClause:
 66            complexity++
 67        }
 68        return true
 69    })
 70    return complexity
 71}
 72
 73// Check 2: Defer in loop
 74func checkDeferInLoop(pass *analysis.Pass, call *ast.CallExpr) {
 75    if ident, ok := call.Fun.(*ast.Ident); ok && ident.Name == "defer" {
 76        if isInLoop(pass, call) {
 77            pass.Report(analysis.Diagnostic{
 78                Pos:     call.Pos(),
 79                Message: "defer in loop may cause resource exhaustion",
 80                SuggestedFixes: []analysis.SuggestedFix{{
 81                    Message: "extract to separate function",
 82                }},
 83            })
 84        }
 85    }
 86}
 87
 88func isInLoop(pass *analysis.Pass, node ast.Node) bool {
 89    var inLoop bool
 90    for _, n := range pass.File.Decls {
 91        ast.Inspect(n, func(parent ast.Node) bool {
 92            if parent == node {
 93                return false
 94            }
 95            switch parent.(type) {
 96            case *ast.ForStmt, *ast.RangeStmt:
 97                inLoop = true
 98            }
 99            return true
100        })
101    }
102    return inLoop
103}
104
105// Check 3: Range over value copy
106func checkRangeValue(pass *analysis.Pass, rng *ast.RangeStmt) {
107    if rng.Value == nil {
108        return
109    }
110
111    valueIdent, ok := rng.Value.(*ast.Ident)
112    if !ok {
113        return
114    }
115
116    // Check if ranging over large struct
117    rangeType := pass.TypesInfo.TypeOf(rng.X)
118    if rangeType == nil {
119        return
120    }
121
122    if slice, ok := rangeType.Underlying().(*types.Slice); ok {
123        elem := slice.Elem()
124        if structType, ok := elem.Underlying().(*types.Struct); ok {
125            size := pass.TypesSizes.Sizeof(structType)
126            if size > 256 {
127                pass.Report(analysis.Diagnostic{
128                    Pos: valueIdent.Pos(),
129                    Message: "ranging over large struct by value; " +
130                        "consider using pointer or index",
131                })
132            }
133        }
134    }
135}
136
137// Check 4: Error variable shadowing
138func checkErrorShadowing(pass *analysis.Pass, assign *ast.AssignStmt) {
139    for _, lhs := range assign.Lhs {
140        if ident, ok := lhs.(*ast.Ident); ok && ident.Name == "err" {
141            if assign.Tok == token.DEFINE {
142                // Check if err already exists in outer scope
143                obj := pass.TypesInfo.ObjectOf(ident)
144                if obj != nil {
145                    scope := obj.Parent()
146                    if scope != nil && scope.Lookup("err") != obj {
147                        pass.Reportf(ident.Pos(),
148                            "error variable 'err' shadows outer scope error")
149                    }
150                }
151            }
152        }
153    }
154}
155
156// Additional checks
157
158// Check for unchecked errors
159func checkUncheckedError(pass *analysis.Pass, call *ast.CallExpr) {
160    // Check if function returns error type
161    if tuple, ok := pass.TypesInfo.TypeOf(call).(*types.Tuple); ok {
162        for i := 0; i < tuple.Len(); i++ {
163            if isErrorType(tuple.At(i).Type()) {
164                // Check if error is ignored
165                if !isErrorHandled(pass, call) {
166                    pass.Reportf(call.Pos(), "error return value not checked")
167                }
168            }
169        }
170    }
171}
172
173func isErrorType(t types.Type) bool {
174    named, ok := t.(*types.Named)
175    if !ok {
176        return false
177    }
178    return named.Obj().Name() == "error"
179}
180
181func isErrorHandled(pass *analysis.Pass, call *ast.CallExpr) bool {
182    // Check if call is in assignment or if statement
183    for _, file := range pass.Files {
184        var handled bool
185        ast.Inspect(file, func(n ast.Node) bool {
186            switch parent := n.(type) {
187            case *ast.AssignStmt:
188                for _, rhs := range parent.Rhs {
189                    if rhs == call {
190                        handled = true
191                    }
192                }
193            case *ast.IfStmt:
194                if parent.Init != nil {
195                    if assign, ok := parent.Init.(*ast.AssignStmt); ok {
196                        for _, rhs := range assign.Rhs {
197                            if rhs == call {
198                                handled = true
199                            }
200                        }
201                    }
202                }
203            }
204            return !handled
205        })
206        if handled {
207            return true
208        }
209    }
210    return false
211}

Usage Example

As standalone tool:

 1package main
 2
 3import (
 4    "golang.org/x/tools/go/analysis/singlechecker"
 5    "yourmodule/analyzer"
 6)
 7
 8func main() {
 9    singlechecker.Main(analyzer.Analyzer)
10}

Run the analyzer:

1go run main.go ./...

Integrate with golangci-lint:

Create plugin.go:

 1package main
 2
 3import (
 4    "golang.org/x/tools/go/analysis"
 5    "yourmodule/analyzer"
 6)
 7
 8// AnalyzerPlugin is required by golangci-lint
 9type AnalyzerPlugin struct{}
10
11func GetAnalyzers() []*analysis.Analyzer {
12    return []*analysis.Analyzer{
13        analyzer.Analyzer,
14    }
15}
16
17var Plugin AnalyzerPlugin

Build as plugin:

1go build -buildmode=plugin -o customcheck.so plugin.go

.golangci.yml configuration:

1linters-settings:
2  custom:
3    customcheck:
4      path: ./customcheck.so
5      description: Custom static analysis checks
6      original-url: github.com/yourorg/customcheck

Key Takeaways

  • go/analysis provides framework for static analysis
  • AST inspection enables deep code understanding
  • Type information helps detect semantic errors
  • Suggested fixes improve developer experience
  • Plugins integrate with existing toolchains
  • Multiple passes enable complex analysis
  • Performance matters for large codebases
  • Clear diagnostics improve code quality