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