366 lines
9.5 KiB
Go
366 lines
9.5 KiB
Go
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")
|
|
} |