aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/linters.go
diff options
context:
space:
mode:
authorRobin Jarry <robin@jarry.cc>2023-01-05 12:16:34 +0100
committerRobin Jarry <robin@jarry.cc>2023-01-06 23:51:52 +0100
commit175c1c822084d0f42892729c8e095397bd830966 (patch)
tree332ef46037e29509eb5ee9466ec2e0e2e12e2ee3 /contrib/linters.go
parent10b995f0196c8243132c3f435d4e11b3c9700e35 (diff)
downloadaerc-175c1c822084d0f42892729c8e095397bd830966.tar.gz
contrib: add linter to check for panic handler in goroutines
If log.PanicHandler() is not installed in a goroutine and a panic occurs, the terminal state is not restored. This causes the panic trace to be unreadable since the terminal is broken. Add a custom analyzer that parses our code and ensures that: defer log.PanicHandler() is the first statement of all functions that are executed in goroutines. Include that linter in golangci config. Signed-off-by: Robin Jarry <robin@jarry.cc> Tested-by: Bence Ferdinandy <bence@ferdinandy.com>
Diffstat (limited to 'contrib/linters.go')
-rw-r--r--contrib/linters.go128
1 files changed, 128 insertions, 0 deletions
diff --git a/contrib/linters.go b/contrib/linters.go
new file mode 100644
index 00000000..7876cab6
--- /dev/null
+++ b/contrib/linters.go
@@ -0,0 +1,128 @@
+package main
+
+import (
+ "go/ast"
+ "go/token"
+ "go/types"
+
+ "golang.org/x/tools/go/analysis"
+)
+
+var PanicAnalyzer = &analysis.Analyzer{
+ Name: "panic",
+ Doc: "finds goroutines that do not initialize the panic handler",
+ Run: runPanic,
+}
+
+func runPanic(pass *analysis.Pass) (interface{}, error) {
+ methods := make(map[token.Pos]string)
+ for _, file := range pass.Files {
+ ast.Inspect(file, func(n ast.Node) bool {
+ g, ok := n.(*ast.GoStmt)
+ if !ok {
+ return true
+ }
+
+ var block *ast.BlockStmt
+
+ expr := g.Call.Fun
+ if e, ok := expr.(*ast.ParenExpr); ok {
+ expr = e.X
+ }
+
+ switch e := expr.(type) {
+ case *ast.FuncLit:
+ block = e.Body
+ case *ast.SelectorExpr:
+ sel, ok := pass.TypesInfo.Selections[e]
+ if ok {
+ f, ok := sel.Obj().(*types.Func)
+ if ok {
+ methods[f.Pos()] = f.Name()
+ }
+ }
+ case *ast.Ident:
+ block = inlineFuncBody(e)
+ }
+
+ if block == nil {
+ return true
+ }
+
+ if !isPanicHandlerInstall(block.List[0]) {
+ pass.Report(panicDiag(block.Pos()))
+ }
+
+ return true
+ })
+ }
+ for _, file := range pass.Files {
+ ast.Inspect(file, func(n ast.Node) bool {
+ f, ok := n.(*ast.FuncDecl)
+ if !ok {
+ return false
+ }
+ _, found := methods[f.Name.Pos()]
+ if !found {
+ return false
+ }
+ delete(methods, f.Name.Pos())
+ if !isPanicHandlerInstall(f.Body.List[0]) {
+ pass.Report(panicDiag(f.Body.Pos()))
+ }
+ return false
+ })
+ }
+
+ return nil, nil
+}
+
+func panicDiag(pos token.Pos) analysis.Diagnostic {
+ return analysis.Diagnostic{
+ Pos: pos,
+ Category: "panic",
+ Message: "missing defer log.PanicHandler() as first statement",
+ }
+}
+
+func inlineFuncBody(s *ast.Ident) *ast.BlockStmt {
+ d, ok := s.Obj.Decl.(*ast.AssignStmt)
+ if !ok {
+ return nil
+ }
+ for _, r := range d.Rhs {
+ if f, ok := r.(*ast.FuncLit); ok {
+ return f.Body
+ }
+ }
+ return nil
+}
+
+func isPanicHandlerInstall(stmt ast.Stmt) bool {
+ d, ok := stmt.(*ast.DeferStmt)
+ if !ok {
+ return false
+ }
+ s, ok := d.Call.Fun.(*ast.SelectorExpr)
+ if !ok {
+ return false
+ }
+ i, ok := s.X.(*ast.Ident)
+ if !ok {
+ return false
+ }
+ return i.Name == "log" && s.Sel.Name == "PanicHandler"
+}
+
+// golang-lint required plugin api
+type analyzerPlugin struct{}
+
+// This must be implemented
+func (*analyzerPlugin) GetAnalyzers() []*analysis.Analyzer {
+ return []*analysis.Analyzer{
+ PanicAnalyzer,
+ }
+}
+
+// This must be defined and named 'AnalyzerPlugin'
+var AnalyzerPlugin analyzerPlugin