_tools/customlint/emptycase.go (90 lines of code) (raw):

package customlint import ( "go/ast" "go/token" "slices" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" ) var emptyCaseAnalyzer = &analysis.Analyzer{ Name: "emptycase", Doc: "finds empty switch/select cases", Run: runEmptyCase, Requires: []*analysis.Analyzer{ inspect.Analyzer, }, } func runEmptyCase(pass *analysis.Pass) (any, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) nodeFilter := []ast.Node{ (*ast.File)(nil), (*ast.SwitchStmt)(nil), (*ast.SelectStmt)(nil), } // The inspect package doesn't tell us up front which file is being used, // so keep track of it as part of the traversal. The file is the first node // so will be set before any other nodes are visited. var file *ast.File inspect.Preorder(nodeFilter, func(n ast.Node) { switch n := n.(type) { case *ast.File: file = n case *ast.SwitchStmt: checkCases(pass, file, n.Body) case *ast.SelectStmt: checkCases(pass, file, n.Body) } }) return nil, nil } func checkCases(pass *analysis.Pass, file *ast.File, clause *ast.BlockStmt) { endOfBlock := clause.End() for i, stmt := range clause.List { nextCasePos := endOfBlock if next := i + 1; next < len(clause.List) { nextCasePos = clause.List[next].Pos() } checkCaseStatement(pass, file, stmt, nextCasePos) } } func checkCaseStatement(pass *analysis.Pass, file *ast.File, stmt ast.Stmt, nextCasePos token.Pos) { var body []ast.Stmt var colon token.Pos switch stmt := stmt.(type) { case *ast.CaseClause: body = stmt.Body colon = stmt.Colon case *ast.CommClause: body = stmt.Body colon = stmt.Colon default: return } if len(body) == 1 { // Also error on a case statement containing a single empty block. block, ok := body[0].(*ast.BlockStmt) if !ok || len(block.List) != 0 { return } } else if len(body) != 0 { return } afterColon := colon + 1 if _, found := slices.BinarySearchFunc(file.Comments, posRange{afterColon, nextCasePos}, posRangeCmp); found { return } pass.Report(analysis.Diagnostic{ Pos: stmt.Pos(), End: afterColon, Message: "this case block is empty and will do nothing", }) } type posRange struct { start, end token.Pos } func posRangeCmp(c *ast.CommentGroup, target posRange) int { if c.End() < target.start { return -1 } if c.Pos() >= target.end { return 1 } return 0 }