173 lines
3.8 KiB
Go
173 lines
3.8 KiB
Go
package main
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// Position represents a location in source code
|
|
type Position struct {
|
|
File string `json:"file"`
|
|
Line int `json:"line"`
|
|
Column int `json:"column"`
|
|
Offset int `json:"offset"` // byte offset in file
|
|
}
|
|
|
|
// newPosition creates a Position from a token.Position
|
|
func newPosition(pos token.Position) Position {
|
|
return Position{
|
|
File: pos.Filename,
|
|
Line: pos.Line,
|
|
Column: pos.Column,
|
|
Offset: pos.Offset,
|
|
}
|
|
}
|
|
|
|
type fileVisitor func(path string, src []byte, file *ast.File, fset *token.FileSet) error
|
|
|
|
func walkGoFiles(dir string, visitor fileVisitor) error {
|
|
fset := token.NewFileSet()
|
|
|
|
return filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if d.IsDir() || !strings.HasSuffix(path, ".go") || strings.Contains(path, "vendor/") {
|
|
return nil
|
|
}
|
|
|
|
src, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
file, err := parser.ParseFile(fset, path, src, parser.ParseComments)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
return visitor(path, src, file, fset)
|
|
})
|
|
}
|
|
|
|
func exprToString(expr ast.Expr) string {
|
|
switch e := expr.(type) {
|
|
case *ast.Ident:
|
|
return e.Name
|
|
case *ast.StarExpr:
|
|
return "*" + exprToString(e.X)
|
|
case *ast.SelectorExpr:
|
|
return exprToString(e.X) + "." + e.Sel.Name
|
|
case *ast.ArrayType:
|
|
if e.Len == nil {
|
|
return "[]" + exprToString(e.Elt)
|
|
}
|
|
return "[" + exprToString(e.Len) + "]" + exprToString(e.Elt)
|
|
case *ast.MapType:
|
|
return "map[" + exprToString(e.Key) + "]" + exprToString(e.Value)
|
|
case *ast.InterfaceType:
|
|
if len(e.Methods.List) == 0 {
|
|
return "interface{}"
|
|
}
|
|
return "interface{...}"
|
|
case *ast.FuncType:
|
|
return funcSignature(e)
|
|
case *ast.ChanType:
|
|
switch e.Dir {
|
|
case ast.SEND:
|
|
return "chan<- " + exprToString(e.Value)
|
|
case ast.RECV:
|
|
return "<-chan " + exprToString(e.Value)
|
|
default:
|
|
return "chan " + exprToString(e.Value)
|
|
}
|
|
case *ast.BasicLit:
|
|
return e.Value
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
func funcSignature(fn *ast.FuncType) string {
|
|
params := fieldListToString(fn.Params)
|
|
results := fieldListToString(fn.Results)
|
|
|
|
if results == "" {
|
|
return "func(" + params + ")"
|
|
}
|
|
return "func(" + params + ") " + results
|
|
}
|
|
|
|
func fieldListToString(fl *ast.FieldList) string {
|
|
if fl == nil || len(fl.List) == 0 {
|
|
return ""
|
|
}
|
|
|
|
var parts []string
|
|
for _, field := range fl.List {
|
|
fieldType := exprToString(field.Type)
|
|
if len(field.Names) == 0 {
|
|
parts = append(parts, fieldType)
|
|
} else {
|
|
for _, name := range field.Names {
|
|
parts = append(parts, name.Name+" "+fieldType)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(parts) == 1 && !strings.Contains(parts[0], " ") {
|
|
return parts[0]
|
|
}
|
|
return "(" + strings.Join(parts, ", ") + ")"
|
|
}
|
|
|
|
func extractContext(src []byte, pos token.Position) string {
|
|
lines := strings.Split(string(src), "\n")
|
|
if pos.Line <= 0 || pos.Line > len(lines) {
|
|
return ""
|
|
}
|
|
|
|
start := pos.Line - 2
|
|
if start < 0 {
|
|
start = 0
|
|
}
|
|
end := pos.Line + 1
|
|
if end > len(lines) {
|
|
end = len(lines)
|
|
}
|
|
|
|
context := strings.Join(lines[start:end], "\n")
|
|
return strings.TrimSpace(context)
|
|
}
|
|
|
|
func extractDocString(doc *ast.CommentGroup) string {
|
|
if doc == nil {
|
|
return ""
|
|
}
|
|
var text strings.Builder
|
|
for _, comment := range doc.List {
|
|
text.WriteString(strings.TrimPrefix(comment.Text, "//"))
|
|
text.WriteString(" ")
|
|
}
|
|
return strings.TrimSpace(text.String())
|
|
}
|
|
|
|
func isErrorCheck(ifStmt *ast.IfStmt) bool {
|
|
// Check if this is an "if err != nil" pattern
|
|
if binExpr, ok := ifStmt.Cond.(*ast.BinaryExpr); ok {
|
|
if binExpr.Op == token.NEQ {
|
|
if ident, ok := binExpr.X.(*ast.Ident); ok && (ident.Name == "err" || strings.Contains(ident.Name, "error")) {
|
|
if ident2, ok := binExpr.Y.(*ast.Ident); ok && ident2.Name == "nil" {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
} |