Add 9 new AST analysis tools with complete position info
This commit is contained in:
114
ast.go
114
ast.go
@@ -11,22 +11,37 @@ import (
|
|||||||
"strings"
|
"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 Symbol struct {
|
type Symbol struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Package string `json:"package"`
|
Package string `json:"package"`
|
||||||
File string `json:"file"`
|
Exported bool `json:"exported"`
|
||||||
Line int `json:"line"`
|
Position Position `json:"position"`
|
||||||
Column int `json:"column"`
|
|
||||||
Exported bool `json:"exported"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type TypeInfo struct {
|
type TypeInfo struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Package string `json:"package"`
|
Package string `json:"package"`
|
||||||
File string `json:"file"`
|
|
||||||
Line int `json:"line"`
|
|
||||||
Kind string `json:"kind"`
|
Kind string `json:"kind"`
|
||||||
|
Position Position `json:"position"`
|
||||||
Fields []FieldInfo `json:"fields,omitempty"`
|
Fields []FieldInfo `json:"fields,omitempty"`
|
||||||
Methods []MethodInfo `json:"methods,omitempty"`
|
Methods []MethodInfo `json:"methods,omitempty"`
|
||||||
Embedded []string `json:"embedded,omitempty"`
|
Embedded []string `json:"embedded,omitempty"`
|
||||||
@@ -35,25 +50,25 @@ type TypeInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FieldInfo struct {
|
type FieldInfo struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Tag string `json:"tag,omitempty"`
|
Tag string `json:"tag,omitempty"`
|
||||||
Exported bool `json:"exported"`
|
Exported bool `json:"exported"`
|
||||||
|
Position Position `json:"position"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MethodInfo struct {
|
type MethodInfo struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Signature string `json:"signature"`
|
Signature string `json:"signature"`
|
||||||
Receiver string `json:"receiver,omitempty"`
|
Receiver string `json:"receiver,omitempty"`
|
||||||
Exported bool `json:"exported"`
|
Exported bool `json:"exported"`
|
||||||
|
Position Position `json:"position"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Reference struct {
|
type Reference struct {
|
||||||
File string `json:"file"`
|
Context string `json:"context"`
|
||||||
Line int `json:"line"`
|
Kind string `json:"kind"`
|
||||||
Column int `json:"column"`
|
Position Position `json:"position"`
|
||||||
Context string `json:"context"`
|
|
||||||
Kind string `json:"kind"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Package struct {
|
type Package struct {
|
||||||
@@ -112,10 +127,8 @@ func findSymbols(dir string, pattern string) ([]Symbol, error) {
|
|||||||
Name: name,
|
Name: name,
|
||||||
Type: "function",
|
Type: "function",
|
||||||
Package: pkgName,
|
Package: pkgName,
|
||||||
File: path,
|
|
||||||
Line: pos.Line,
|
|
||||||
Column: pos.Column,
|
|
||||||
Exported: ast.IsExported(name),
|
Exported: ast.IsExported(name),
|
||||||
|
Position: newPosition(pos),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,10 +150,8 @@ func findSymbols(dir string, pattern string) ([]Symbol, error) {
|
|||||||
Name: name,
|
Name: name,
|
||||||
Type: kind,
|
Type: kind,
|
||||||
Package: pkgName,
|
Package: pkgName,
|
||||||
File: path,
|
|
||||||
Line: pos.Line,
|
|
||||||
Column: pos.Column,
|
|
||||||
Exported: ast.IsExported(name),
|
Exported: ast.IsExported(name),
|
||||||
|
Position: newPosition(pos),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,10 +167,8 @@ func findSymbols(dir string, pattern string) ([]Symbol, error) {
|
|||||||
Name: name.Name,
|
Name: name.Name,
|
||||||
Type: kind,
|
Type: kind,
|
||||||
Package: pkgName,
|
Package: pkgName,
|
||||||
File: path,
|
|
||||||
Line: pos.Line,
|
|
||||||
Column: pos.Column,
|
|
||||||
Exported: ast.IsExported(name.Name),
|
Exported: ast.IsExported(name.Name),
|
||||||
|
Position: newPosition(pos),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,21 +203,20 @@ func getTypeInfo(dir string, typeName string) (*TypeInfo, error) {
|
|||||||
if ts, ok := spec.(*ast.TypeSpec); ok && ts.Name.Name == typeName {
|
if ts, ok := spec.(*ast.TypeSpec); ok && ts.Name.Name == typeName {
|
||||||
pos := fset.Position(ts.Pos())
|
pos := fset.Position(ts.Pos())
|
||||||
info := &TypeInfo{
|
info := &TypeInfo{
|
||||||
Name: typeName,
|
Name: typeName,
|
||||||
Package: file.Name.Name,
|
Package: file.Name.Name,
|
||||||
File: path,
|
Position: newPosition(pos),
|
||||||
Line: pos.Line,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch t := ts.Type.(type) {
|
switch t := ts.Type.(type) {
|
||||||
case *ast.StructType:
|
case *ast.StructType:
|
||||||
info.Kind = "struct"
|
info.Kind = "struct"
|
||||||
info.Fields = extractFields(t)
|
info.Fields = extractFields(t, fset)
|
||||||
info.Embedded = extractEmbedded(t)
|
info.Embedded = extractEmbedded(t)
|
||||||
|
|
||||||
case *ast.InterfaceType:
|
case *ast.InterfaceType:
|
||||||
info.Kind = "interface"
|
info.Kind = "interface"
|
||||||
info.Interface = extractInterfaceMethods(t)
|
info.Interface = extractInterfaceMethods(t, fset)
|
||||||
|
|
||||||
case *ast.Ident:
|
case *ast.Ident:
|
||||||
info.Kind = "alias"
|
info.Kind = "alias"
|
||||||
@@ -224,7 +232,7 @@ func getTypeInfo(dir string, typeName string) (*TypeInfo, error) {
|
|||||||
info.Kind = "other"
|
info.Kind = "other"
|
||||||
}
|
}
|
||||||
|
|
||||||
info.Methods = extractMethods(file, typeName)
|
info.Methods = extractMethods(file, typeName, fset)
|
||||||
result = info
|
result = info
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -257,11 +265,9 @@ func findReferences(dir string, symbol string) ([]Reference, error) {
|
|||||||
context := extractContext(src, pos)
|
context := extractContext(src, pos)
|
||||||
|
|
||||||
refs = append(refs, Reference{
|
refs = append(refs, Reference{
|
||||||
File: path,
|
Context: context,
|
||||||
Line: pos.Line,
|
Kind: kind,
|
||||||
Column: pos.Column,
|
Position: newPosition(pos),
|
||||||
Context: context,
|
|
||||||
Kind: kind,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,11 +277,9 @@ func findReferences(dir string, symbol string) ([]Reference, error) {
|
|||||||
context := extractContext(src, pos)
|
context := extractContext(src, pos)
|
||||||
|
|
||||||
refs = append(refs, Reference{
|
refs = append(refs, Reference{
|
||||||
File: path,
|
Context: context,
|
||||||
Line: pos.Line,
|
Kind: "selector",
|
||||||
Column: pos.Column,
|
Position: newPosition(pos),
|
||||||
Context: context,
|
|
||||||
Kind: "selector",
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -362,7 +366,7 @@ func matchesPattern(name, pattern string) bool {
|
|||||||
return strings.Contains(name, pattern)
|
return strings.Contains(name, pattern)
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractFields(st *ast.StructType) []FieldInfo {
|
func extractFields(st *ast.StructType, fset *token.FileSet) []FieldInfo {
|
||||||
var fields []FieldInfo
|
var fields []FieldInfo
|
||||||
|
|
||||||
for _, field := range st.Fields.List {
|
for _, field := range st.Fields.List {
|
||||||
@@ -373,19 +377,23 @@ func extractFields(st *ast.StructType) []FieldInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(field.Names) == 0 {
|
if len(field.Names) == 0 {
|
||||||
|
pos := fset.Position(field.Pos())
|
||||||
fields = append(fields, FieldInfo{
|
fields = append(fields, FieldInfo{
|
||||||
Name: "",
|
Name: "",
|
||||||
Type: fieldType,
|
Type: fieldType,
|
||||||
Tag: tag,
|
Tag: tag,
|
||||||
Exported: true,
|
Exported: true,
|
||||||
|
Position: newPosition(pos),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
for _, name := range field.Names {
|
for _, name := range field.Names {
|
||||||
|
pos := fset.Position(name.Pos())
|
||||||
fields = append(fields, FieldInfo{
|
fields = append(fields, FieldInfo{
|
||||||
Name: name.Name,
|
Name: name.Name,
|
||||||
Type: fieldType,
|
Type: fieldType,
|
||||||
Tag: tag,
|
Tag: tag,
|
||||||
Exported: ast.IsExported(name.Name),
|
Exported: ast.IsExported(name.Name),
|
||||||
|
Position: newPosition(pos),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -406,17 +414,19 @@ func extractEmbedded(st *ast.StructType) []string {
|
|||||||
return embedded
|
return embedded
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractInterfaceMethods(it *ast.InterfaceType) []MethodInfo {
|
func extractInterfaceMethods(it *ast.InterfaceType, fset *token.FileSet) []MethodInfo {
|
||||||
var methods []MethodInfo
|
var methods []MethodInfo
|
||||||
|
|
||||||
for _, method := range it.Methods.List {
|
for _, method := range it.Methods.List {
|
||||||
if len(method.Names) > 0 {
|
if len(method.Names) > 0 {
|
||||||
for _, name := range method.Names {
|
for _, name := range method.Names {
|
||||||
sig := exprToString(method.Type)
|
sig := exprToString(method.Type)
|
||||||
|
pos := fset.Position(name.Pos())
|
||||||
methods = append(methods, MethodInfo{
|
methods = append(methods, MethodInfo{
|
||||||
Name: name.Name,
|
Name: name.Name,
|
||||||
Signature: sig,
|
Signature: sig,
|
||||||
Exported: ast.IsExported(name.Name),
|
Exported: ast.IsExported(name.Name),
|
||||||
|
Position: newPosition(pos),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -425,7 +435,7 @@ func extractInterfaceMethods(it *ast.InterfaceType) []MethodInfo {
|
|||||||
return methods
|
return methods
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractMethods(file *ast.File, typeName string) []MethodInfo {
|
func extractMethods(file *ast.File, typeName string, fset *token.FileSet) []MethodInfo {
|
||||||
var methods []MethodInfo
|
var methods []MethodInfo
|
||||||
|
|
||||||
for _, decl := range file.Decls {
|
for _, decl := range file.Decls {
|
||||||
@@ -434,11 +444,13 @@ func extractMethods(file *ast.File, typeName string) []MethodInfo {
|
|||||||
recvType := exprToString(recv.Type)
|
recvType := exprToString(recv.Type)
|
||||||
if strings.Contains(recvType, typeName) {
|
if strings.Contains(recvType, typeName) {
|
||||||
sig := funcSignature(fn.Type)
|
sig := funcSignature(fn.Type)
|
||||||
|
pos := fset.Position(fn.Name.Pos())
|
||||||
methods = append(methods, MethodInfo{
|
methods = append(methods, MethodInfo{
|
||||||
Name: fn.Name.Name,
|
Name: fn.Name.Name,
|
||||||
Signature: sig,
|
Signature: sig,
|
||||||
Receiver: recvType,
|
Receiver: recvType,
|
||||||
Exported: ast.IsExported(fn.Name.Name),
|
Exported: ast.IsExported(fn.Name.Name),
|
||||||
|
Position: newPosition(pos),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
904
ast_extended.go
Normal file
904
ast_extended.go
Normal file
@@ -0,0 +1,904 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go/ast"
|
||||||
|
"go/token"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Import analysis types
|
||||||
|
type ImportInfo struct {
|
||||||
|
Package string `json:"package"`
|
||||||
|
File string `json:"file"`
|
||||||
|
Imports []ImportDetail `json:"imports"`
|
||||||
|
UnusedImports []string `json:"unused_imports,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImportDetail struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
Alias string `json:"alias,omitempty"`
|
||||||
|
Used []string `json:"used_symbols,omitempty"`
|
||||||
|
Position Position `json:"position"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function call types
|
||||||
|
type FunctionCall struct {
|
||||||
|
Caller string `json:"caller"`
|
||||||
|
Context string `json:"context"`
|
||||||
|
Position Position `json:"position"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Struct usage types
|
||||||
|
type StructUsage struct {
|
||||||
|
File string `json:"file"`
|
||||||
|
Literals []StructLiteral `json:"literals,omitempty"`
|
||||||
|
FieldAccess []FieldAccess `json:"field_access,omitempty"`
|
||||||
|
TypeUsage []TypeUsage `json:"type_usage,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StructLiteral struct {
|
||||||
|
Fields []string `json:"fields_initialized"`
|
||||||
|
IsComposite bool `json:"is_composite"`
|
||||||
|
Position Position `json:"position"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FieldAccess struct {
|
||||||
|
Field string `json:"field"`
|
||||||
|
Context string `json:"context"`
|
||||||
|
Position Position `json:"position"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TypeUsage struct {
|
||||||
|
Usage string `json:"usage"`
|
||||||
|
Position Position `json:"position"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface analysis types
|
||||||
|
type InterfaceInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Package string `json:"package"`
|
||||||
|
Position Position `json:"position"`
|
||||||
|
Methods []MethodInfo `json:"methods"`
|
||||||
|
Implementations []ImplementationType `json:"implementations,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImplementationType struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Package string `json:"package"`
|
||||||
|
Position Position `json:"position"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error handling types
|
||||||
|
type ErrorInfo struct {
|
||||||
|
File string `json:"file"`
|
||||||
|
UnhandledErrors []ErrorContext `json:"unhandled_errors,omitempty"`
|
||||||
|
ErrorChecks []ErrorContext `json:"error_checks,omitempty"`
|
||||||
|
ErrorReturns []ErrorContext `json:"error_returns,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorContext struct {
|
||||||
|
Context string `json:"context"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Position Position `json:"position"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test analysis types
|
||||||
|
type TestAnalysis struct {
|
||||||
|
TestFiles []TestFile `json:"test_files"`
|
||||||
|
ExportedFunctions []ExportedFunc `json:"exported_functions"`
|
||||||
|
TestCoverage TestCoverage `json:"coverage_summary"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestFile struct {
|
||||||
|
File string `json:"file"`
|
||||||
|
Package string `json:"package"`
|
||||||
|
Tests []string `json:"tests"`
|
||||||
|
Benchmarks []string `json:"benchmarks,omitempty"`
|
||||||
|
Examples []string `json:"examples,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExportedFunc struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Package string `json:"package"`
|
||||||
|
Tested bool `json:"tested"`
|
||||||
|
Position Position `json:"position"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestCoverage struct {
|
||||||
|
TotalExported int `json:"total_exported"`
|
||||||
|
TotalTested int `json:"total_tested"`
|
||||||
|
Percentage float64 `json:"percentage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comment analysis types
|
||||||
|
type CommentInfo struct {
|
||||||
|
File string `json:"file"`
|
||||||
|
TODOs []CommentItem `json:"todos,omitempty"`
|
||||||
|
Undocumented []CommentItem `json:"undocumented,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommentItem struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Comment string `json:"comment,omitempty"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Position Position `json:"position"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dependency analysis types
|
||||||
|
type DependencyInfo struct {
|
||||||
|
Package string `json:"package"`
|
||||||
|
Dir string `json:"dir"`
|
||||||
|
Dependencies []string `json:"dependencies"`
|
||||||
|
Dependents []string `json:"dependents,omitempty"`
|
||||||
|
Cycles [][]string `json:"cycles,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic types
|
||||||
|
type GenericInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Package string `json:"package"`
|
||||||
|
Position Position `json:"position"`
|
||||||
|
TypeParams []TypeParam `json:"type_params"`
|
||||||
|
Instances []Instance `json:"instances,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TypeParam struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Constraint string `json:"constraint"`
|
||||||
|
Position Position `json:"position"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Instance struct {
|
||||||
|
Types []string `json:"types"`
|
||||||
|
Position Position `json:"position"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func findImports(dir string) ([]ImportInfo, error) {
|
||||||
|
var imports []ImportInfo
|
||||||
|
|
||||||
|
err := walkGoFiles(dir, func(path string, src []byte, file *ast.File, fset *token.FileSet) error {
|
||||||
|
info := ImportInfo{
|
||||||
|
Package: file.Name.Name,
|
||||||
|
File: path,
|
||||||
|
Imports: []ImportDetail{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect all imports
|
||||||
|
importMap := make(map[string]*ImportDetail)
|
||||||
|
for _, imp := range file.Imports {
|
||||||
|
importPath := strings.Trim(imp.Path.Value, `"`)
|
||||||
|
pos := fset.Position(imp.Pos())
|
||||||
|
detail := &ImportDetail{
|
||||||
|
Path: importPath,
|
||||||
|
Position: newPosition(pos),
|
||||||
|
}
|
||||||
|
if imp.Name != nil {
|
||||||
|
detail.Alias = imp.Name.Name
|
||||||
|
}
|
||||||
|
importMap[importPath] = detail
|
||||||
|
info.Imports = append(info.Imports, *detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track which imports are used
|
||||||
|
usedImports := make(map[string]map[string]bool)
|
||||||
|
ast.Inspect(file, func(n ast.Node) bool {
|
||||||
|
switch x := n.(type) {
|
||||||
|
case *ast.SelectorExpr:
|
||||||
|
if ident, ok := x.X.(*ast.Ident); ok {
|
||||||
|
pkgName := ident.Name
|
||||||
|
symbol := x.Sel.Name
|
||||||
|
|
||||||
|
// Find matching import
|
||||||
|
for importPath, detail := range importMap {
|
||||||
|
importName := filepath.Base(importPath)
|
||||||
|
if detail.Alias != "" && detail.Alias == pkgName {
|
||||||
|
if usedImports[importPath] == nil {
|
||||||
|
usedImports[importPath] = make(map[string]bool)
|
||||||
|
}
|
||||||
|
usedImports[importPath][symbol] = true
|
||||||
|
} else if importName == pkgName {
|
||||||
|
if usedImports[importPath] == nil {
|
||||||
|
usedImports[importPath] = make(map[string]bool)
|
||||||
|
}
|
||||||
|
usedImports[importPath][symbol] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update import details with used symbols
|
||||||
|
for i, imp := range info.Imports {
|
||||||
|
if used, ok := usedImports[imp.Path]; ok {
|
||||||
|
for symbol := range used {
|
||||||
|
info.Imports[i].Used = append(info.Imports[i].Used, symbol)
|
||||||
|
}
|
||||||
|
} else if !strings.HasSuffix(imp.Path, "_test") && imp.Alias != "_" {
|
||||||
|
info.UnusedImports = append(info.UnusedImports, imp.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(info.Imports) > 0 {
|
||||||
|
imports = append(imports, info)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return imports, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func findFunctionCalls(dir string, functionName string) ([]FunctionCall, error) {
|
||||||
|
var calls []FunctionCall
|
||||||
|
|
||||||
|
err := walkGoFiles(dir, func(path string, src []byte, file *ast.File, fset *token.FileSet) error {
|
||||||
|
currentFunc := ""
|
||||||
|
|
||||||
|
ast.Inspect(file, func(n ast.Node) bool {
|
||||||
|
// Track current function context
|
||||||
|
if fn, ok := n.(*ast.FuncDecl); ok {
|
||||||
|
currentFunc = fn.Name.Name
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find function calls
|
||||||
|
switch x := n.(type) {
|
||||||
|
case *ast.CallExpr:
|
||||||
|
var calledName string
|
||||||
|
switch fun := x.Fun.(type) {
|
||||||
|
case *ast.Ident:
|
||||||
|
calledName = fun.Name
|
||||||
|
case *ast.SelectorExpr:
|
||||||
|
calledName = fun.Sel.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
if calledName == functionName {
|
||||||
|
pos := fset.Position(x.Pos())
|
||||||
|
context := extractContext(src, pos)
|
||||||
|
|
||||||
|
calls = append(calls, FunctionCall{
|
||||||
|
Caller: currentFunc,
|
||||||
|
Context: context,
|
||||||
|
Position: newPosition(pos),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return calls, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func findStructUsage(dir string, structName string) ([]StructUsage, error) {
|
||||||
|
var usages []StructUsage
|
||||||
|
|
||||||
|
err := walkGoFiles(dir, func(path string, src []byte, file *ast.File, fset *token.FileSet) error {
|
||||||
|
usage := StructUsage{
|
||||||
|
File: path,
|
||||||
|
}
|
||||||
|
|
||||||
|
ast.Inspect(file, func(n ast.Node) bool {
|
||||||
|
switch x := n.(type) {
|
||||||
|
// Find struct literals
|
||||||
|
case *ast.CompositeLit:
|
||||||
|
if typeName := getTypeName(x.Type); typeName == structName {
|
||||||
|
pos := fset.Position(x.Pos())
|
||||||
|
lit := StructLiteral{
|
||||||
|
IsComposite: len(x.Elts) > 0,
|
||||||
|
Position: newPosition(pos),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract initialized fields
|
||||||
|
for _, elt := range x.Elts {
|
||||||
|
if kv, ok := elt.(*ast.KeyValueExpr); ok {
|
||||||
|
if ident, ok := kv.Key.(*ast.Ident); ok {
|
||||||
|
lit.Fields = append(lit.Fields, ident.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
usage.Literals = append(usage.Literals, lit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find field access
|
||||||
|
case *ast.SelectorExpr:
|
||||||
|
if typeName := getTypeName(x.X); strings.Contains(typeName, structName) {
|
||||||
|
pos := fset.Position(x.Sel.Pos())
|
||||||
|
context := extractContext(src, pos)
|
||||||
|
|
||||||
|
usage.FieldAccess = append(usage.FieldAccess, FieldAccess{
|
||||||
|
Field: x.Sel.Name,
|
||||||
|
Context: context,
|
||||||
|
Position: newPosition(pos),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find type usage in declarations
|
||||||
|
case *ast.Field:
|
||||||
|
if typeName := getTypeName(x.Type); typeName == structName {
|
||||||
|
pos := fset.Position(x.Pos())
|
||||||
|
usage.TypeUsage = append(usage.TypeUsage, TypeUsage{
|
||||||
|
Usage: "field",
|
||||||
|
Position: newPosition(pos),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(usage.Literals) > 0 || len(usage.FieldAccess) > 0 || len(usage.TypeUsage) > 0 {
|
||||||
|
usages = append(usages, usage)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return usages, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractInterfaces(dir string, interfaceName string) ([]InterfaceInfo, error) {
|
||||||
|
var interfaces []InterfaceInfo
|
||||||
|
interfaceMap := make(map[string]*InterfaceInfo)
|
||||||
|
|
||||||
|
// First pass: collect all interfaces
|
||||||
|
err := walkGoFiles(dir, func(path string, src []byte, file *ast.File, fset *token.FileSet) error {
|
||||||
|
ast.Inspect(file, func(n ast.Node) bool {
|
||||||
|
if genDecl, ok := n.(*ast.GenDecl); ok {
|
||||||
|
for _, spec := range genDecl.Specs {
|
||||||
|
if typeSpec, ok := spec.(*ast.TypeSpec); ok {
|
||||||
|
if iface, ok := typeSpec.Type.(*ast.InterfaceType); ok {
|
||||||
|
name := typeSpec.Name.Name
|
||||||
|
if interfaceName == "" || name == interfaceName {
|
||||||
|
pos := fset.Position(typeSpec.Pos())
|
||||||
|
info := &InterfaceInfo{
|
||||||
|
Name: name,
|
||||||
|
Package: file.Name.Name,
|
||||||
|
Position: newPosition(pos),
|
||||||
|
Methods: extractInterfaceMethods(iface, fset),
|
||||||
|
}
|
||||||
|
interfaceMap[name] = info
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second pass: find implementations
|
||||||
|
if interfaceName != "" {
|
||||||
|
iface, exists := interfaceMap[interfaceName]
|
||||||
|
if exists {
|
||||||
|
err = walkGoFiles(dir, func(path string, src []byte, file *ast.File, fset *token.FileSet) error {
|
||||||
|
// Collect all types with methods
|
||||||
|
types := make(map[string][]string)
|
||||||
|
|
||||||
|
for _, decl := range file.Decls {
|
||||||
|
if fn, ok := decl.(*ast.FuncDecl); ok && fn.Recv != nil {
|
||||||
|
for _, recv := range fn.Recv.List {
|
||||||
|
typeName := getTypeName(recv.Type)
|
||||||
|
types[typeName] = append(types[typeName], fn.Name.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any type implements the interface
|
||||||
|
for typeName, methods := range types {
|
||||||
|
if implementsInterface(methods, iface.Methods) {
|
||||||
|
// Find type declaration
|
||||||
|
ast.Inspect(file, func(n ast.Node) bool {
|
||||||
|
if genDecl, ok := n.(*ast.GenDecl); ok {
|
||||||
|
for _, spec := range genDecl.Specs {
|
||||||
|
if typeSpec, ok := spec.(*ast.TypeSpec); ok && typeSpec.Name.Name == typeName {
|
||||||
|
pos := fset.Position(typeSpec.Pos())
|
||||||
|
iface.Implementations = append(iface.Implementations, ImplementationType{
|
||||||
|
Type: typeName,
|
||||||
|
Package: file.Name.Name,
|
||||||
|
Position: newPosition(pos),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert map to slice
|
||||||
|
for _, iface := range interfaceMap {
|
||||||
|
interfaces = append(interfaces, *iface)
|
||||||
|
}
|
||||||
|
|
||||||
|
return interfaces, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func findErrors(dir string) ([]ErrorInfo, error) {
|
||||||
|
var errors []ErrorInfo
|
||||||
|
|
||||||
|
err := walkGoFiles(dir, func(path string, src []byte, file *ast.File, fset *token.FileSet) error {
|
||||||
|
info := ErrorInfo{
|
||||||
|
File: path,
|
||||||
|
}
|
||||||
|
|
||||||
|
ast.Inspect(file, func(n ast.Node) bool {
|
||||||
|
switch x := n.(type) {
|
||||||
|
// Find function calls that return errors but aren't checked
|
||||||
|
case *ast.ExprStmt:
|
||||||
|
if call, ok := x.X.(*ast.CallExpr); ok {
|
||||||
|
// Check if this function likely returns an error
|
||||||
|
if returnsError(call, file) {
|
||||||
|
pos := fset.Position(call.Pos())
|
||||||
|
context := extractContext(src, pos)
|
||||||
|
info.UnhandledErrors = append(info.UnhandledErrors, ErrorContext{
|
||||||
|
Context: context,
|
||||||
|
Type: "unchecked_call",
|
||||||
|
Position: newPosition(pos),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find error checks
|
||||||
|
case *ast.IfStmt:
|
||||||
|
if isErrorCheck(x) {
|
||||||
|
pos := fset.Position(x.Pos())
|
||||||
|
context := extractContext(src, pos)
|
||||||
|
info.ErrorChecks = append(info.ErrorChecks, ErrorContext{
|
||||||
|
Context: context,
|
||||||
|
Type: "error_check",
|
||||||
|
Position: newPosition(pos),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find error returns
|
||||||
|
case *ast.ReturnStmt:
|
||||||
|
for _, result := range x.Results {
|
||||||
|
if ident, ok := result.(*ast.Ident); ok && (ident.Name == "err" || strings.Contains(ident.Name, "error")) {
|
||||||
|
pos := fset.Position(x.Pos())
|
||||||
|
context := extractContext(src, pos)
|
||||||
|
info.ErrorReturns = append(info.ErrorReturns, ErrorContext{
|
||||||
|
Context: context,
|
||||||
|
Type: "error_return",
|
||||||
|
Position: newPosition(pos),
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(info.UnhandledErrors) > 0 || len(info.ErrorChecks) > 0 || len(info.ErrorReturns) > 0 {
|
||||||
|
errors = append(errors, info)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return errors, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func analyzeTests(dir string) (*TestAnalysis, error) {
|
||||||
|
analysis := &TestAnalysis{
|
||||||
|
TestFiles: []TestFile{},
|
||||||
|
ExportedFunctions: []ExportedFunc{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect all exported functions
|
||||||
|
exportedFuncs := make(map[string]*ExportedFunc)
|
||||||
|
|
||||||
|
err := walkGoFiles(dir, func(path string, src []byte, file *ast.File, fset *token.FileSet) error {
|
||||||
|
if strings.HasSuffix(path, "_test.go") {
|
||||||
|
// Process test files
|
||||||
|
testFile := TestFile{
|
||||||
|
File: path,
|
||||||
|
Package: file.Name.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, decl := range file.Decls {
|
||||||
|
if fn, ok := decl.(*ast.FuncDecl); ok {
|
||||||
|
name := fn.Name.Name
|
||||||
|
if strings.HasPrefix(name, "Test") {
|
||||||
|
testFile.Tests = append(testFile.Tests, name)
|
||||||
|
} else if strings.HasPrefix(name, "Benchmark") {
|
||||||
|
testFile.Benchmarks = append(testFile.Benchmarks, name)
|
||||||
|
} else if strings.HasPrefix(name, "Example") {
|
||||||
|
testFile.Examples = append(testFile.Examples, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(testFile.Tests) > 0 || len(testFile.Benchmarks) > 0 || len(testFile.Examples) > 0 {
|
||||||
|
analysis.TestFiles = append(analysis.TestFiles, testFile)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Collect exported functions
|
||||||
|
for _, decl := range file.Decls {
|
||||||
|
if fn, ok := decl.(*ast.FuncDecl); ok && ast.IsExported(fn.Name.Name) {
|
||||||
|
key := file.Name.Name + "." + fn.Name.Name
|
||||||
|
pos := fset.Position(fn.Pos())
|
||||||
|
exportedFuncs[key] = &ExportedFunc{
|
||||||
|
Name: fn.Name.Name,
|
||||||
|
Package: file.Name.Name,
|
||||||
|
Tested: false,
|
||||||
|
Position: newPosition(pos),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check which functions are tested
|
||||||
|
for _, testFile := range analysis.TestFiles {
|
||||||
|
for _, testName := range testFile.Tests {
|
||||||
|
// Simple heuristic: TestFunctionName tests FunctionName
|
||||||
|
funcName := strings.TrimPrefix(testName, "Test")
|
||||||
|
key := testFile.Package + "." + funcName
|
||||||
|
if fn, exists := exportedFuncs[key]; exists {
|
||||||
|
fn.Tested = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert map to slice and calculate coverage
|
||||||
|
tested := 0
|
||||||
|
for _, fn := range exportedFuncs {
|
||||||
|
analysis.ExportedFunctions = append(analysis.ExportedFunctions, *fn)
|
||||||
|
if fn.Tested {
|
||||||
|
tested++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
analysis.TestCoverage = TestCoverage{
|
||||||
|
TotalExported: len(exportedFuncs),
|
||||||
|
TotalTested: tested,
|
||||||
|
}
|
||||||
|
if len(exportedFuncs) > 0 {
|
||||||
|
analysis.TestCoverage.Percentage = float64(tested) / float64(len(exportedFuncs)) * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
return analysis, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findComments(dir string, commentType string) ([]CommentInfo, error) {
|
||||||
|
var comments []CommentInfo
|
||||||
|
|
||||||
|
err := walkGoFiles(dir, func(path string, src []byte, file *ast.File, fset *token.FileSet) error {
|
||||||
|
info := CommentInfo{
|
||||||
|
File: path,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find TODOs in comments
|
||||||
|
if commentType == "todo" || commentType == "all" {
|
||||||
|
todoRegex := regexp.MustCompile(`(?i)\b(todo|fixme|hack|bug|xxx)\b`)
|
||||||
|
for _, cg := range file.Comments {
|
||||||
|
for _, c := range cg.List {
|
||||||
|
if todoRegex.MatchString(c.Text) {
|
||||||
|
pos := fset.Position(c.Pos())
|
||||||
|
info.TODOs = append(info.TODOs, CommentItem{
|
||||||
|
Comment: c.Text,
|
||||||
|
Type: "todo",
|
||||||
|
Position: newPosition(pos),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find undocumented exported symbols
|
||||||
|
if commentType == "undocumented" || commentType == "all" {
|
||||||
|
ast.Inspect(file, func(n ast.Node) bool {
|
||||||
|
switch x := n.(type) {
|
||||||
|
case *ast.FuncDecl:
|
||||||
|
if ast.IsExported(x.Name.Name) && x.Doc == nil {
|
||||||
|
pos := fset.Position(x.Pos())
|
||||||
|
info.Undocumented = append(info.Undocumented, CommentItem{
|
||||||
|
Name: x.Name.Name,
|
||||||
|
Type: "function",
|
||||||
|
Position: newPosition(pos),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case *ast.GenDecl:
|
||||||
|
for _, spec := range x.Specs {
|
||||||
|
switch s := spec.(type) {
|
||||||
|
case *ast.TypeSpec:
|
||||||
|
if ast.IsExported(s.Name.Name) && x.Doc == nil && s.Doc == nil {
|
||||||
|
pos := fset.Position(s.Pos())
|
||||||
|
info.Undocumented = append(info.Undocumented, CommentItem{
|
||||||
|
Name: s.Name.Name,
|
||||||
|
Type: "type",
|
||||||
|
Position: newPosition(pos),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case *ast.ValueSpec:
|
||||||
|
for _, name := range s.Names {
|
||||||
|
if ast.IsExported(name.Name) && x.Doc == nil && s.Doc == nil {
|
||||||
|
pos := fset.Position(name.Pos())
|
||||||
|
info.Undocumented = append(info.Undocumented, CommentItem{
|
||||||
|
Name: name.Name,
|
||||||
|
Type: "value",
|
||||||
|
Position: newPosition(pos),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(info.TODOs) > 0 || len(info.Undocumented) > 0 {
|
||||||
|
comments = append(comments, info)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return comments, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func analyzeDependencies(dir string) ([]DependencyInfo, error) {
|
||||||
|
depMap := make(map[string]*DependencyInfo)
|
||||||
|
|
||||||
|
// First pass: collect all packages and their imports
|
||||||
|
err := walkGoFiles(dir, func(path string, src []byte, file *ast.File, fset *token.FileSet) error {
|
||||||
|
pkgDir := filepath.Dir(path)
|
||||||
|
if _, exists := depMap[pkgDir]; !exists {
|
||||||
|
depMap[pkgDir] = &DependencyInfo{
|
||||||
|
Package: file.Name.Name,
|
||||||
|
Dir: pkgDir,
|
||||||
|
Dependencies: []string{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add imports
|
||||||
|
for _, imp := range file.Imports {
|
||||||
|
importPath := strings.Trim(imp.Path.Value, `"`)
|
||||||
|
if !contains(depMap[pkgDir].Dependencies, importPath) {
|
||||||
|
depMap[pkgDir].Dependencies = append(depMap[pkgDir].Dependencies, importPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build dependency graph and find cycles
|
||||||
|
var deps []DependencyInfo
|
||||||
|
for _, dep := range depMap {
|
||||||
|
// Find internal dependencies
|
||||||
|
for _, imp := range dep.Dependencies {
|
||||||
|
// Check if this is an internal package
|
||||||
|
for otherDir, otherDep := range depMap {
|
||||||
|
if strings.HasSuffix(imp, otherDep.Package) && otherDir != dep.Dir {
|
||||||
|
otherDep.Dependents = append(otherDep.Dependents, dep.Package)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deps = append(deps, *dep)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple cycle detection (could be enhanced)
|
||||||
|
for i := range deps {
|
||||||
|
deps[i].Cycles = findCycles(&deps[i], depMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
return deps, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findGenerics(dir string) ([]GenericInfo, error) {
|
||||||
|
var generics []GenericInfo
|
||||||
|
|
||||||
|
err := walkGoFiles(dir, func(path string, src []byte, file *ast.File, fset *token.FileSet) error {
|
||||||
|
ast.Inspect(file, func(n ast.Node) bool {
|
||||||
|
switch x := n.(type) {
|
||||||
|
case *ast.GenDecl:
|
||||||
|
for _, spec := range x.Specs {
|
||||||
|
if ts, ok := spec.(*ast.TypeSpec); ok && ts.TypeParams != nil {
|
||||||
|
pos := fset.Position(ts.Pos())
|
||||||
|
info := GenericInfo{
|
||||||
|
Name: ts.Name.Name,
|
||||||
|
Kind: "type",
|
||||||
|
Package: file.Name.Name,
|
||||||
|
Position: newPosition(pos),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract type parameters
|
||||||
|
for _, param := range ts.TypeParams.List {
|
||||||
|
for _, name := range param.Names {
|
||||||
|
namePos := fset.Position(name.Pos())
|
||||||
|
tp := TypeParam{
|
||||||
|
Name: name.Name,
|
||||||
|
Position: newPosition(namePos),
|
||||||
|
}
|
||||||
|
if param.Type != nil {
|
||||||
|
tp.Constraint = exprToString(param.Type)
|
||||||
|
}
|
||||||
|
info.TypeParams = append(info.TypeParams, tp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generics = append(generics, info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case *ast.FuncDecl:
|
||||||
|
if x.Type.TypeParams != nil {
|
||||||
|
pos := fset.Position(x.Pos())
|
||||||
|
info := GenericInfo{
|
||||||
|
Name: x.Name.Name,
|
||||||
|
Kind: "function",
|
||||||
|
Package: file.Name.Name,
|
||||||
|
Position: newPosition(pos),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract type parameters
|
||||||
|
for _, param := range x.Type.TypeParams.List {
|
||||||
|
for _, name := range param.Names {
|
||||||
|
namePos := fset.Position(name.Pos())
|
||||||
|
tp := TypeParam{
|
||||||
|
Name: name.Name,
|
||||||
|
Position: newPosition(namePos),
|
||||||
|
}
|
||||||
|
if param.Type != nil {
|
||||||
|
tp.Constraint = exprToString(param.Type)
|
||||||
|
}
|
||||||
|
info.TypeParams = append(info.TypeParams, tp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generics = append(generics, info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return generics, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
|
||||||
|
func getTypeName(expr ast.Expr) string {
|
||||||
|
switch x := expr.(type) {
|
||||||
|
case *ast.Ident:
|
||||||
|
return x.Name
|
||||||
|
case *ast.StarExpr:
|
||||||
|
return getTypeName(x.X)
|
||||||
|
case *ast.SelectorExpr:
|
||||||
|
return exprToString(x)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func implementsInterface(methods []string, interfaceMethods []MethodInfo) bool {
|
||||||
|
for _, im := range interfaceMethods {
|
||||||
|
found := false
|
||||||
|
for _, m := range methods {
|
||||||
|
if m == im.Name {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func returnsError(call *ast.CallExpr, file *ast.File) bool {
|
||||||
|
// Simple heuristic: check if the function name suggests it returns an error
|
||||||
|
switch fun := call.Fun.(type) {
|
||||||
|
case *ast.Ident:
|
||||||
|
name := fun.Name
|
||||||
|
return strings.HasPrefix(name, "New") || strings.HasPrefix(name, "Create") ||
|
||||||
|
strings.HasPrefix(name, "Open") || strings.HasPrefix(name, "Read") ||
|
||||||
|
strings.HasPrefix(name, "Write") || strings.HasPrefix(name, "Parse")
|
||||||
|
case *ast.SelectorExpr:
|
||||||
|
name := fun.Sel.Name
|
||||||
|
return strings.HasPrefix(name, "New") || strings.HasPrefix(name, "Create") ||
|
||||||
|
strings.HasPrefix(name, "Open") || strings.HasPrefix(name, "Read") ||
|
||||||
|
strings.HasPrefix(name, "Write") || strings.HasPrefix(name, "Parse")
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(slice []string, str string) bool {
|
||||||
|
for _, s := range slice {
|
||||||
|
if s == str {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func findCycles(dep *DependencyInfo, depMap map[string]*DependencyInfo) [][]string {
|
||||||
|
// Simple DFS-based cycle detection
|
||||||
|
var cycles [][]string
|
||||||
|
visited := make(map[string]bool)
|
||||||
|
recStack := make(map[string]bool)
|
||||||
|
path := []string{}
|
||||||
|
|
||||||
|
var dfs func(pkg string) bool
|
||||||
|
dfs = func(pkg string) bool {
|
||||||
|
visited[pkg] = true
|
||||||
|
recStack[pkg] = true
|
||||||
|
path = append(path, pkg)
|
||||||
|
|
||||||
|
// Find dependencies for this package
|
||||||
|
for _, d := range depMap {
|
||||||
|
if d.Package == pkg {
|
||||||
|
for _, imp := range d.Dependencies {
|
||||||
|
for _, otherDep := range depMap {
|
||||||
|
if strings.HasSuffix(imp, otherDep.Package) {
|
||||||
|
if !visited[otherDep.Package] {
|
||||||
|
if dfs(otherDep.Package) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else if recStack[otherDep.Package] {
|
||||||
|
// Found a cycle
|
||||||
|
cycleStart := -1
|
||||||
|
for i, p := range path {
|
||||||
|
if p == otherDep.Package {
|
||||||
|
cycleStart = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cycleStart >= 0 {
|
||||||
|
cycle := append([]string{}, path[cycleStart:]...)
|
||||||
|
cycles = append(cycles, cycle)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path = path[:len(path)-1]
|
||||||
|
recStack[pkg] = false
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
dfs(dep.Package)
|
||||||
|
return cycles
|
||||||
|
}
|
||||||
249
main.go
249
main.go
@@ -91,6 +91,101 @@ func main() {
|
|||||||
)
|
)
|
||||||
mcpServer.AddTool(listPackagesTool, listPackagesHandler)
|
mcpServer.AddTool(listPackagesTool, listPackagesHandler)
|
||||||
|
|
||||||
|
// Define the find_imports tool
|
||||||
|
findImportsTool := mcp.NewTool("find_imports",
|
||||||
|
mcp.WithDescription("Analyze import usage and find unused imports"),
|
||||||
|
mcp.WithString("dir",
|
||||||
|
mcp.Description("Directory to search (default: current directory)"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
mcpServer.AddTool(findImportsTool, findImportsHandler)
|
||||||
|
|
||||||
|
// Define the find_function_calls tool
|
||||||
|
findFunctionCallsTool := mcp.NewTool("find_function_calls",
|
||||||
|
mcp.WithDescription("Find all calls to a specific function"),
|
||||||
|
mcp.WithString("dir",
|
||||||
|
mcp.Description("Directory to search (default: current directory)"),
|
||||||
|
),
|
||||||
|
mcp.WithString("function",
|
||||||
|
mcp.Required(),
|
||||||
|
mcp.Description("Function name to find calls for"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
mcpServer.AddTool(findFunctionCallsTool, findFunctionCallsHandler)
|
||||||
|
|
||||||
|
// Define the find_struct_usage tool
|
||||||
|
findStructUsageTool := mcp.NewTool("find_struct_usage",
|
||||||
|
mcp.WithDescription("Find struct instantiations and field access patterns"),
|
||||||
|
mcp.WithString("dir",
|
||||||
|
mcp.Description("Directory to search (default: current directory)"),
|
||||||
|
),
|
||||||
|
mcp.WithString("struct",
|
||||||
|
mcp.Required(),
|
||||||
|
mcp.Description("Struct name to analyze usage for"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
mcpServer.AddTool(findStructUsageTool, findStructUsageHandler)
|
||||||
|
|
||||||
|
// Define the extract_interfaces tool
|
||||||
|
extractInterfacesTool := mcp.NewTool("extract_interfaces",
|
||||||
|
mcp.WithDescription("Find types implementing an interface or suggest interfaces"),
|
||||||
|
mcp.WithString("dir",
|
||||||
|
mcp.Description("Directory to search (default: current directory)"),
|
||||||
|
),
|
||||||
|
mcp.WithString("interface",
|
||||||
|
mcp.Description("Interface name to find implementations for (if empty, lists all interfaces)"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
mcpServer.AddTool(extractInterfacesTool, extractInterfacesHandler)
|
||||||
|
|
||||||
|
// Define the find_errors tool
|
||||||
|
findErrorsTool := mcp.NewTool("find_errors",
|
||||||
|
mcp.WithDescription("Find error handling patterns and unhandled errors"),
|
||||||
|
mcp.WithString("dir",
|
||||||
|
mcp.Description("Directory to search (default: current directory)"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
mcpServer.AddTool(findErrorsTool, findErrorsHandler)
|
||||||
|
|
||||||
|
// Define the analyze_tests tool
|
||||||
|
analyzeTestsTool := mcp.NewTool("analyze_tests",
|
||||||
|
mcp.WithDescription("Analyze test coverage and find untested exported functions"),
|
||||||
|
mcp.WithString("dir",
|
||||||
|
mcp.Description("Directory to search (default: current directory)"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
mcpServer.AddTool(analyzeTestsTool, analyzeTestsHandler)
|
||||||
|
|
||||||
|
// Define the find_comments tool
|
||||||
|
findCommentsTool := mcp.NewTool("find_comments",
|
||||||
|
mcp.WithDescription("Find undocumented exports, TODOs, and analyze comments"),
|
||||||
|
mcp.WithString("dir",
|
||||||
|
mcp.Description("Directory to search (default: current directory)"),
|
||||||
|
),
|
||||||
|
mcp.WithString("type",
|
||||||
|
mcp.Description("Comment type to find: 'todo', 'undocumented', or 'all' (default: 'all')"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
mcpServer.AddTool(findCommentsTool, findCommentsHandler)
|
||||||
|
|
||||||
|
// Define the analyze_dependencies tool
|
||||||
|
analyzeDependenciesTool := mcp.NewTool("analyze_dependencies",
|
||||||
|
mcp.WithDescription("Analyze package dependencies and find cycles"),
|
||||||
|
mcp.WithString("dir",
|
||||||
|
mcp.Description("Directory to search (default: current directory)"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
mcpServer.AddTool(analyzeDependenciesTool, analyzeDependenciesHandler)
|
||||||
|
|
||||||
|
// Define the find_generics tool
|
||||||
|
findGenericsTool := mcp.NewTool("find_generics",
|
||||||
|
mcp.WithDescription("Find generic types, functions and their instantiations"),
|
||||||
|
mcp.WithString("dir",
|
||||||
|
mcp.Description("Directory to search (default: current directory)"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
mcpServer.AddTool(findGenericsTool, findGenericsHandler)
|
||||||
|
|
||||||
// Start the server
|
// Start the server
|
||||||
if err := server.ServeStdio(mcpServer); err != nil {
|
if err := server.ServeStdio(mcpServer); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Server error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Server error: %v\n", err)
|
||||||
@@ -244,5 +339,159 @@ func listPackagesHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp
|
|||||||
return mcp.NewToolResultError(fmt.Sprintf("failed to marshal packages: %v", err)), nil
|
return mcp.NewToolResultError(fmt.Sprintf("failed to marshal packages: %v", err)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return mcp.NewToolResultText(string(jsonData)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findImportsHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
dir := request.GetString("dir", "./")
|
||||||
|
|
||||||
|
imports, err := findImports(dir)
|
||||||
|
if err != nil {
|
||||||
|
return mcp.NewToolResultError(fmt.Sprintf("failed to analyze imports: %v", err)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(imports)
|
||||||
|
if err != nil {
|
||||||
|
return mcp.NewToolResultError(fmt.Sprintf("failed to marshal imports: %v", err)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return mcp.NewToolResultText(string(jsonData)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findFunctionCallsHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
dir := request.GetString("dir", "./")
|
||||||
|
function, err := request.RequireString("function")
|
||||||
|
if err != nil {
|
||||||
|
return mcp.NewToolResultError(err.Error()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
calls, err := findFunctionCalls(dir, function)
|
||||||
|
if err != nil {
|
||||||
|
return mcp.NewToolResultError(fmt.Sprintf("failed to find function calls: %v", err)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(calls)
|
||||||
|
if err != nil {
|
||||||
|
return mcp.NewToolResultError(fmt.Sprintf("failed to marshal calls: %v", err)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return mcp.NewToolResultText(string(jsonData)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findStructUsageHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
dir := request.GetString("dir", "./")
|
||||||
|
structName, err := request.RequireString("struct")
|
||||||
|
if err != nil {
|
||||||
|
return mcp.NewToolResultError(err.Error()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
usage, err := findStructUsage(dir, structName)
|
||||||
|
if err != nil {
|
||||||
|
return mcp.NewToolResultError(fmt.Sprintf("failed to find struct usage: %v", err)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(usage)
|
||||||
|
if err != nil {
|
||||||
|
return mcp.NewToolResultError(fmt.Sprintf("failed to marshal usage: %v", err)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return mcp.NewToolResultText(string(jsonData)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractInterfacesHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
dir := request.GetString("dir", "./")
|
||||||
|
interfaceName := request.GetString("interface", "")
|
||||||
|
|
||||||
|
interfaces, err := extractInterfaces(dir, interfaceName)
|
||||||
|
if err != nil {
|
||||||
|
return mcp.NewToolResultError(fmt.Sprintf("failed to extract interfaces: %v", err)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(interfaces)
|
||||||
|
if err != nil {
|
||||||
|
return mcp.NewToolResultError(fmt.Sprintf("failed to marshal interfaces: %v", err)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return mcp.NewToolResultText(string(jsonData)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findErrorsHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
dir := request.GetString("dir", "./")
|
||||||
|
|
||||||
|
errors, err := findErrors(dir)
|
||||||
|
if err != nil {
|
||||||
|
return mcp.NewToolResultError(fmt.Sprintf("failed to find errors: %v", err)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(errors)
|
||||||
|
if err != nil {
|
||||||
|
return mcp.NewToolResultError(fmt.Sprintf("failed to marshal errors: %v", err)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return mcp.NewToolResultText(string(jsonData)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func analyzeTestsHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
dir := request.GetString("dir", "./")
|
||||||
|
|
||||||
|
analysis, err := analyzeTests(dir)
|
||||||
|
if err != nil {
|
||||||
|
return mcp.NewToolResultError(fmt.Sprintf("failed to analyze tests: %v", err)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(analysis)
|
||||||
|
if err != nil {
|
||||||
|
return mcp.NewToolResultError(fmt.Sprintf("failed to marshal analysis: %v", err)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return mcp.NewToolResultText(string(jsonData)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findCommentsHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
dir := request.GetString("dir", "./")
|
||||||
|
commentType := request.GetString("type", "all")
|
||||||
|
|
||||||
|
comments, err := findComments(dir, commentType)
|
||||||
|
if err != nil {
|
||||||
|
return mcp.NewToolResultError(fmt.Sprintf("failed to find comments: %v", err)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(comments)
|
||||||
|
if err != nil {
|
||||||
|
return mcp.NewToolResultError(fmt.Sprintf("failed to marshal comments: %v", err)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return mcp.NewToolResultText(string(jsonData)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func analyzeDependenciesHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
dir := request.GetString("dir", "./")
|
||||||
|
|
||||||
|
deps, err := analyzeDependencies(dir)
|
||||||
|
if err != nil {
|
||||||
|
return mcp.NewToolResultError(fmt.Sprintf("failed to analyze dependencies: %v", err)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(deps)
|
||||||
|
if err != nil {
|
||||||
|
return mcp.NewToolResultError(fmt.Sprintf("failed to marshal dependencies: %v", err)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return mcp.NewToolResultText(string(jsonData)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findGenericsHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
dir := request.GetString("dir", "./")
|
||||||
|
|
||||||
|
generics, err := findGenerics(dir)
|
||||||
|
if err != nil {
|
||||||
|
return mcp.NewToolResultError(fmt.Sprintf("failed to find generics: %v", err)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(generics)
|
||||||
|
if err != nil {
|
||||||
|
return mcp.NewToolResultError(fmt.Sprintf("failed to marshal generics: %v", err)), nil
|
||||||
|
}
|
||||||
|
|
||||||
return mcp.NewToolResultText(string(jsonData)), nil
|
return mcp.NewToolResultText(string(jsonData)), nil
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user