Add AST analysis tools and go run/test execution tools
This commit is contained in:
366
tool_find_empty_blocks.go
Normal file
366
tool_find_empty_blocks.go
Normal file
@@ -0,0 +1,366 @@
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user