Files
gocp/tool_find_empty_blocks.go

366 lines
9.5 KiB
Go
Raw Permalink Normal View History

package main
import (
"go/ast"
"go/token"
"strings"
)
type EmptyBlock struct {
Type string `json:"type"` // "if", "else", "for", "switch_case", "function", etc.
Description string `json:"description"`
Position Position `json:"position"`
Context string `json:"context"`
}
type EmptyBlockAnalysis struct {
EmptyBlocks []EmptyBlock `json:"empty_blocks"`
Issues []EmptyBlockIssue `json:"issues"`
}
type EmptyBlockIssue struct {
Type string `json:"type"`
Description string `json:"description"`
Position Position `json:"position"`
}
func findEmptyBlocks(dir string) (*EmptyBlockAnalysis, error) {
analysis := &EmptyBlockAnalysis{
EmptyBlocks: []EmptyBlock{},
Issues: []EmptyBlockIssue{},
}
err := walkGoFiles(dir, func(path string, src []byte, file *ast.File, fset *token.FileSet) error {
ast.Inspect(file, func(n ast.Node) bool {
switch node := n.(type) {
case *ast.IfStmt:
analyzeIfStatement(node, fset, src, analysis)
case *ast.ForStmt:
if isEmptyBlock(node.Body) {
pos := fset.Position(node.Pos())
empty := EmptyBlock{
Type: "for",
Description: "Empty for loop",
Position: newPosition(pos),
Context: extractContext(src, pos),
}
analysis.EmptyBlocks = append(analysis.EmptyBlocks, empty)
// Check if it's an infinite loop
if node.Cond == nil && node.Init == nil && node.Post == nil {
issue := EmptyBlockIssue{
Type: "empty_infinite_loop",
Description: "Empty infinite loop - possible bug or incomplete implementation",
Position: newPosition(pos),
}
analysis.Issues = append(analysis.Issues, issue)
}
}
case *ast.RangeStmt:
if isEmptyBlock(node.Body) {
pos := fset.Position(node.Pos())
empty := EmptyBlock{
Type: "range",
Description: "Empty range loop",
Position: newPosition(pos),
Context: extractContext(src, pos),
}
analysis.EmptyBlocks = append(analysis.EmptyBlocks, empty)
}
case *ast.SwitchStmt:
analyzeSwitchStatement(node, fset, src, analysis)
case *ast.TypeSwitchStmt:
analyzeTypeSwitchStatement(node, fset, src, analysis)
case *ast.FuncDecl:
if node.Body != nil && isEmptyBlock(node.Body) {
pos := fset.Position(node.Pos())
empty := EmptyBlock{
Type: "function",
Description: "Empty function: " + node.Name.Name,
Position: newPosition(pos),
Context: extractContext(src, pos),
}
analysis.EmptyBlocks = append(analysis.EmptyBlocks, empty)
// Check if it's an interface stub
if !isInterfaceStub(node) && !isTestHelper(node.Name.Name) {
issue := EmptyBlockIssue{
Type: "empty_function",
Description: "Function '" + node.Name.Name + "' has empty body",
Position: newPosition(pos),
}
analysis.Issues = append(analysis.Issues, issue)
}
}
case *ast.BlockStmt:
// Check for standalone empty blocks
if isEmptyBlock(node) && !isPartOfControlStructure(file, node) {
pos := fset.Position(node.Pos())
empty := EmptyBlock{
Type: "block",
Description: "Empty code block",
Position: newPosition(pos),
Context: extractContext(src, pos),
}
analysis.EmptyBlocks = append(analysis.EmptyBlocks, empty)
}
}
return true
})
return nil
})
return analysis, err
}
func isEmptyBlock(block *ast.BlockStmt) bool {
if block == nil {
return true
}
// Check if block has no statements
if len(block.List) == 0 {
return true
}
// Check if all statements are empty
for _, stmt := range block.List {
if !isEmptyStatement(stmt) {
return false
}
}
return true
}
func isEmptyStatement(stmt ast.Stmt) bool {
switch s := stmt.(type) {
case *ast.EmptyStmt:
return true
case *ast.BlockStmt:
return isEmptyBlock(s)
default:
return false
}
}
func analyzeIfStatement(ifStmt *ast.IfStmt, fset *token.FileSet, src []byte, analysis *EmptyBlockAnalysis) {
// Check if body
if isEmptyBlock(ifStmt.Body) {
pos := fset.Position(ifStmt.Pos())
empty := EmptyBlock{
Type: "if",
Description: "Empty if block",
Position: newPosition(pos),
Context: extractContext(src, pos),
}
analysis.EmptyBlocks = append(analysis.EmptyBlocks, empty)
// Check if there's an else block
if ifStmt.Else == nil {
issue := EmptyBlockIssue{
Type: "empty_if_no_else",
Description: "Empty if block with no else - condition may be unnecessary",
Position: newPosition(pos),
}
analysis.Issues = append(analysis.Issues, issue)
}
}
// Check else block
if ifStmt.Else != nil {
switch elseNode := ifStmt.Else.(type) {
case *ast.BlockStmt:
if isEmptyBlock(elseNode) {
pos := fset.Position(elseNode.Pos())
empty := EmptyBlock{
Type: "else",
Description: "Empty else block",
Position: newPosition(pos),
Context: extractContext(src, pos),
}
analysis.EmptyBlocks = append(analysis.EmptyBlocks, empty)
issue := EmptyBlockIssue{
Type: "empty_else",
Description: "Empty else block - can be removed",
Position: newPosition(pos),
}
analysis.Issues = append(analysis.Issues, issue)
}
case *ast.IfStmt:
// Recursively analyze else if
analyzeIfStatement(elseNode, fset, src, analysis)
}
}
}
func analyzeSwitchStatement(switchStmt *ast.SwitchStmt, fset *token.FileSet, src []byte, analysis *EmptyBlockAnalysis) {
for _, stmt := range switchStmt.Body.List {
if caseClause, ok := stmt.(*ast.CaseClause); ok {
if len(caseClause.Body) == 0 {
pos := fset.Position(caseClause.Pos())
caseDesc := "default"
if len(caseClause.List) > 0 {
caseDesc = "case"
}
empty := EmptyBlock{
Type: "switch_case",
Description: "Empty " + caseDesc + " clause",
Position: newPosition(pos),
Context: extractContext(src, pos),
}
analysis.EmptyBlocks = append(analysis.EmptyBlocks, empty)
// Check if it's not a fallthrough case
if !hasFallthrough(switchStmt, caseClause) {
issue := EmptyBlockIssue{
Type: "empty_switch_case",
Description: "Empty " + caseDesc + " clause with no fallthrough",
Position: newPosition(pos),
}
analysis.Issues = append(analysis.Issues, issue)
}
}
}
}
}
func analyzeTypeSwitchStatement(typeSwitch *ast.TypeSwitchStmt, fset *token.FileSet, src []byte, analysis *EmptyBlockAnalysis) {
for _, stmt := range typeSwitch.Body.List {
if caseClause, ok := stmt.(*ast.CaseClause); ok {
if len(caseClause.Body) == 0 {
pos := fset.Position(caseClause.Pos())
caseDesc := "default"
if len(caseClause.List) > 0 {
caseDesc = "type case"
}
empty := EmptyBlock{
Type: "type_switch_case",
Description: "Empty " + caseDesc + " clause",
Position: newPosition(pos),
Context: extractContext(src, pos),
}
analysis.EmptyBlocks = append(analysis.EmptyBlocks, empty)
}
}
}
}
func isPartOfControlStructure(file *ast.File, block *ast.BlockStmt) bool {
var isControl bool
ast.Inspect(file, func(n ast.Node) bool {
switch node := n.(type) {
case *ast.IfStmt:
if node.Body == block || node.Else == block {
isControl = true
return false
}
case *ast.ForStmt:
if node.Body == block {
isControl = true
return false
}
case *ast.RangeStmt:
if node.Body == block {
isControl = true
return false
}
case *ast.SwitchStmt:
if node.Body == block {
isControl = true
return false
}
case *ast.TypeSwitchStmt:
if node.Body == block {
isControl = true
return false
}
case *ast.FuncDecl:
if node.Body == block {
isControl = true
return false
}
case *ast.FuncLit:
if node.Body == block {
isControl = true
return false
}
}
return true
})
return isControl
}
func hasFallthrough(switchStmt *ast.SwitchStmt, caseClause *ast.CaseClause) bool {
// Check if the previous case has a fallthrough
var prevCase *ast.CaseClause
for _, stmt := range switchStmt.Body.List {
if cc, ok := stmt.(*ast.CaseClause); ok {
if cc == caseClause && prevCase != nil {
// Check if previous case ends with fallthrough
if len(prevCase.Body) > 0 {
if _, ok := prevCase.Body[len(prevCase.Body)-1].(*ast.BranchStmt); ok {
return true
}
}
}
prevCase = cc
}
}
return false
}
func isInterfaceStub(fn *ast.FuncDecl) bool {
// Check if function has a receiver (method)
if fn.Recv == nil || len(fn.Recv.List) == 0 {
return false
}
// Check for common stub patterns in name
name := fn.Name.Name
stubPatterns := []string{"Stub", "Mock", "Fake", "Dummy", "NoOp", "Noop"}
for _, pattern := range stubPatterns {
if strings.Contains(name, pattern) {
return true
}
}
// Check if receiver type contains stub patterns
if len(fn.Recv.List) > 0 {
recvType := exprToString(fn.Recv.List[0].Type)
for _, pattern := range stubPatterns {
if strings.Contains(recvType, pattern) {
return true
}
}
}
return false
}
func isTestHelper(name string) bool {
// Common test helper patterns
helpers := []string{"setUp", "tearDown", "beforeEach", "afterEach", "beforeAll", "afterAll"}
nameLower := strings.ToLower(name)
for _, helper := range helpers {
if strings.ToLower(helper) == nameLower {
return true
}
}
// Check for test-related prefixes
return strings.HasPrefix(name, "Test") ||
strings.HasPrefix(name, "Benchmark") ||
strings.HasPrefix(name, "Example")
}