From 7f6613e2df4542e8da5f311b06750ec62c21f645 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Fri, 27 Jun 2025 21:04:39 -0700 Subject: [PATCH] Add 14 advanced AST analysis tools for code quality and testing --- ast_advanced.go | 1121 +++++++++++++++++++++++++++++++++++++++++++++++ main.go | 362 +++++++++++++++ 2 files changed, 1483 insertions(+) create mode 100644 ast_advanced.go diff --git a/ast_advanced.go b/ast_advanced.go new file mode 100644 index 0000000..e76be29 --- /dev/null +++ b/ast_advanced.go @@ -0,0 +1,1121 @@ +package main + +import ( + "go/ast" + "go/token" + "crypto/md5" + "fmt" + "strings" +) + +// Dead code analysis types +type DeadCodeInfo struct { + File string `json:"file"` + UnusedVars []UnusedItem `json:"unused_vars,omitempty"` + UnreachableCode []CodeLocation `json:"unreachable_code,omitempty"` + DeadBranches []CodeLocation `json:"dead_branches,omitempty"` +} + +type UnusedItem struct { + Name string `json:"name"` + Type string `json:"type"` + Position Position `json:"position"` +} + +type CodeLocation struct { + Description string `json:"description"` + Position Position `json:"position"` +} + +// Code duplication types +type DuplicateInfo struct { + Similarity float64 `json:"similarity"` + Locations []DuplicateLocation `json:"locations"` + Content string `json:"content"` +} + +type DuplicateLocation struct { + File string `json:"file"` + Function string `json:"function"` + Position Position `json:"position"` +} + +// Performance inefficiency types +type InefficiencyInfo struct { + File string `json:"file"` + StringConcat []InefficiencyItem `json:"string_concat,omitempty"` + Conversions []InefficiencyItem `json:"unnecessary_conversions,omitempty"` + Allocations []InefficiencyItem `json:"potential_allocations,omitempty"` +} + +type InefficiencyItem struct { + Type string `json:"type"` + Description string `json:"description"` + Suggestion string `json:"suggestion"` + Position Position `json:"position"` +} + +// API analysis types +type ApiInfo struct { + Package string `json:"package"` + Functions []ApiFunction `json:"functions"` + Types []ApiType `json:"types"` + Constants []ApiConstant `json:"constants"` + Variables []ApiVariable `json:"variables"` +} + +type ApiFunction struct { + Name string `json:"name"` + Signature string `json:"signature"` + Doc string `json:"doc,omitempty"` + Position Position `json:"position"` +} + +type ApiType struct { + Name string `json:"name"` + Kind string `json:"kind"` + Doc string `json:"doc,omitempty"` + Position Position `json:"position"` +} + +type ApiConstant struct { + Name string `json:"name"` + Type string `json:"type"` + Value string `json:"value,omitempty"` + Doc string `json:"doc,omitempty"` + Position Position `json:"position"` +} + +type ApiVariable struct { + Name string `json:"name"` + Type string `json:"type"` + Doc string `json:"doc,omitempty"` + Position Position `json:"position"` +} + +// Documentation types +type DocInfo struct { + Package string `json:"package"` + Overview string `json:"overview"` + Functions []DocFunction `json:"functions"` + Types []DocType `json:"types"` +} + +type DocFunction struct { + Name string `json:"name"` + Signature string `json:"signature"` + Description string `json:"description"` + Parameters []string `json:"parameters,omitempty"` + Returns []string `json:"returns,omitempty"` + Examples []string `json:"examples,omitempty"` + Position Position `json:"position"` +} + +type DocType struct { + Name string `json:"name"` + Kind string `json:"kind"` + Description string `json:"description"` + Fields []DocField `json:"fields,omitempty"` + Methods []DocMethod `json:"methods,omitempty"` + Position Position `json:"position"` +} + +type DocField struct { + Name string `json:"name"` + Type string `json:"type"` + Description string `json:"description"` +} + +type DocMethod struct { + Name string `json:"name"` + Signature string `json:"signature"` + Description string `json:"description"` +} + +// Deprecated usage types +type DeprecatedInfo struct { + File string `json:"file"` + Usage []DeprecatedUsage `json:"usage"` +} + +type DeprecatedUsage struct { + Item string `json:"item"` + Alternative string `json:"alternative,omitempty"` + Reason string `json:"reason,omitempty"` + Position Position `json:"position"` +} + +// Coupling analysis types +type CouplingInfo struct { + Package string `json:"package"` + Afferent int `json:"afferent"` + Efferent int `json:"efferent"` + Instability float64 `json:"instability"` + Dependencies []string `json:"dependencies"` + Dependents []string `json:"dependents"` + Suggestions []string `json:"suggestions,omitempty"` +} + +// Design pattern types +type PatternInfo struct { + Pattern string `json:"pattern"` + Occurrences []PatternOccurrence `json:"occurrences"` +} + +type PatternOccurrence struct { + File string `json:"file"` + Description string `json:"description"` + Quality string `json:"quality"` + Position Position `json:"position"` +} + +// Architecture analysis types +type ArchitectureInfo struct { + Layers []LayerInfo `json:"layers"` + Violations []LayerViolation `json:"violations,omitempty"` + Suggestions []string `json:"suggestions,omitempty"` +} + +type LayerInfo struct { + Name string `json:"name"` + Packages []string `json:"packages"` + Dependencies []string `json:"dependencies"` +} + +type LayerViolation struct { + From string `json:"from"` + To string `json:"to"` + Violation string `json:"violation"` + Position Position `json:"position"` +} + +// Go idioms types +type IdiomsInfo struct { + File string `json:"file"` + Violations []IdiomItem `json:"violations,omitempty"` + Suggestions []IdiomItem `json:"suggestions,omitempty"` +} + +type IdiomItem struct { + Type string `json:"type"` + Description string `json:"description"` + Suggestion string `json:"suggestion"` + Position Position `json:"position"` +} + +// Context usage types +type ContextInfo struct { + File string `json:"file"` + MissingContext []ContextUsage `json:"missing_context,omitempty"` + ProperUsage []ContextUsage `json:"proper_usage,omitempty"` + ImproperUsage []ContextUsage `json:"improper_usage,omitempty"` +} + +type ContextUsage struct { + Function string `json:"function"` + Type string `json:"type"` + Description string `json:"description"` + Position Position `json:"position"` +} + +// Embedding analysis types +type EmbeddingInfo struct { + File string `json:"file"` + Structs []StructEmbedding `json:"structs,omitempty"` + Interfaces []InterfaceEmbedding `json:"interfaces,omitempty"` +} + +type StructEmbedding struct { + Name string `json:"name"` + Embedded []string `json:"embedded"` + Methods []string `json:"promoted_methods"` + Position Position `json:"position"` +} + +type InterfaceEmbedding struct { + Name string `json:"name"` + Embedded []string `json:"embedded"` + Methods []string `json:"methods"` + Position Position `json:"position"` +} + +// Test quality types +type TestQualityInfo struct { + File string `json:"file"` + TestMetrics TestMetrics `json:"metrics"` + Issues []TestIssue `json:"issues,omitempty"` + Suggestions []string `json:"suggestions,omitempty"` +} + +type TestMetrics struct { + TotalTests int `json:"total_tests"` + TableDriven int `json:"table_driven"` + Benchmarks int `json:"benchmarks"` + Examples int `json:"examples"` + Coverage float64 `json:"estimated_coverage"` +} + +type TestIssue struct { + Type string `json:"type"` + Description string `json:"description"` + Severity string `json:"severity"` + Position Position `json:"position"` +} + +// Missing tests types +type MissingTestInfo struct { + Function string `json:"function"` + Package string `json:"package"` + Complexity int `json:"complexity"` + Criticality string `json:"criticality"` + Reason string `json:"reason"` + Position Position `json:"position"` +} + +func findDeadCode(dir string) ([]DeadCodeInfo, error) { + var deadCode []DeadCodeInfo + + err := walkGoFiles(dir, func(path string, src []byte, file *ast.File, fset *token.FileSet) error { + if strings.HasSuffix(path, "_test.go") { + return nil + } + + info := DeadCodeInfo{ + File: path, + } + + // Track variable usage + declaredVars := make(map[string]*ast.ValueSpec) + usedVars := make(map[string]bool) + + // First pass: collect declared variables + ast.Inspect(file, func(n ast.Node) bool { + if genDecl, ok := n.(*ast.GenDecl); ok && genDecl.Tok == token.VAR { + for _, spec := range genDecl.Specs { + if valueSpec, ok := spec.(*ast.ValueSpec); ok { + for _, name := range valueSpec.Names { + if name.Name != "_" && !ast.IsExported(name.Name) { + declaredVars[name.Name] = valueSpec + } + } + } + } + } + return true + }) + + // Second pass: track usage + ast.Inspect(file, func(n ast.Node) bool { + if ident, ok := n.(*ast.Ident); ok { + usedVars[ident.Name] = true + } + return true + }) + + // Find unused variables + for varName, valueSpec := range declaredVars { + if !usedVars[varName] { + for _, name := range valueSpec.Names { + if name.Name == varName { + pos := fset.Position(name.Pos()) + info.UnusedVars = append(info.UnusedVars, UnusedItem{ + Name: varName, + Type: "variable", + Position: newPosition(pos), + }) + } + } + } + } + + // Find unreachable code (simplified detection) + ast.Inspect(file, func(n ast.Node) bool { + if blockStmt, ok := n.(*ast.BlockStmt); ok { + for i, stmt := range blockStmt.List { + if _, ok := stmt.(*ast.ReturnStmt); ok && i < len(blockStmt.List)-1 { + pos := fset.Position(blockStmt.List[i+1].Pos()) + info.UnreachableCode = append(info.UnreachableCode, CodeLocation{ + Description: "Code after return statement", + Position: newPosition(pos), + }) + } + } + } + return true + }) + + if len(info.UnusedVars) > 0 || len(info.UnreachableCode) > 0 || len(info.DeadBranches) > 0 { + deadCode = append(deadCode, info) + } + return nil + }) + + return deadCode, err +} + +func findDuplicates(dir string, threshold float64) ([]DuplicateInfo, error) { + var duplicates []DuplicateInfo + functionBodies := make(map[string][]DuplicateLocation) + + err := walkGoFiles(dir, func(path string, src []byte, file *ast.File, fset *token.FileSet) error { + ast.Inspect(file, func(n ast.Node) bool { + if fn, ok := n.(*ast.FuncDecl); ok && fn.Body != nil { + body := extractFunctionBody(fn.Body, fset) + hash := fmt.Sprintf("%x", md5.Sum([]byte(body))) + + pos := fset.Position(fn.Pos()) + location := DuplicateLocation{ + File: path, + Function: fn.Name.Name, + Position: newPosition(pos), + } + + functionBodies[hash] = append(functionBodies[hash], location) + } + return true + }) + return nil + }) + + if err != nil { + return nil, err + } + + // Find duplicates + for hash, locations := range functionBodies { + if len(locations) > 1 { + duplicates = append(duplicates, DuplicateInfo{ + Similarity: 1.0, + Locations: locations, + Content: hash, + }) + } + } + + return duplicates, nil +} + +func findInefficiencies(dir string) ([]InefficiencyInfo, error) { + var inefficiencies []InefficiencyInfo + + err := walkGoFiles(dir, func(path string, src []byte, file *ast.File, fset *token.FileSet) error { + info := InefficiencyInfo{ + File: path, + } + + ast.Inspect(file, func(n ast.Node) bool { + // Find string concatenation in loops + if forStmt, ok := n.(*ast.ForStmt); ok { + ast.Inspect(forStmt.Body, func(inner ast.Node) bool { + if binExpr, ok := inner.(*ast.BinaryExpr); ok && binExpr.Op == token.ADD { + if isStringType(binExpr.X) || isStringType(binExpr.Y) { + pos := fset.Position(binExpr.Pos()) + info.StringConcat = append(info.StringConcat, InefficiencyItem{ + Type: "string_concatenation_in_loop", + Description: "String concatenation in loop can be inefficient", + Suggestion: "Consider using strings.Builder", + Position: newPosition(pos), + }) + } + } + return true + }) + } + + // Find unnecessary type conversions + if callExpr, ok := n.(*ast.CallExpr); ok { + if len(callExpr.Args) == 1 { + if ident, ok := callExpr.Fun.(*ast.Ident); ok { + argType := getExprType(callExpr.Args[0]) + if ident.Name == argType { + pos := fset.Position(callExpr.Pos()) + info.Conversions = append(info.Conversions, InefficiencyItem{ + Type: "unnecessary_conversion", + Description: fmt.Sprintf("Unnecessary conversion to %s", ident.Name), + Suggestion: "Remove unnecessary type conversion", + Position: newPosition(pos), + }) + } + } + } + } + + return true + }) + + if len(info.StringConcat) > 0 || len(info.Conversions) > 0 || len(info.Allocations) > 0 { + inefficiencies = append(inefficiencies, info) + } + return nil + }) + + return inefficiencies, err +} + +func extractApi(dir string) ([]ApiInfo, error) { + var apis []ApiInfo + + err := walkGoFiles(dir, func(path string, src []byte, file *ast.File, fset *token.FileSet) error { + if strings.HasSuffix(path, "_test.go") { + return nil + } + + api := ApiInfo{ + Package: file.Name.Name, + } + + for _, decl := range file.Decls { + switch d := decl.(type) { + case *ast.FuncDecl: + if ast.IsExported(d.Name.Name) { + pos := fset.Position(d.Pos()) + api.Functions = append(api.Functions, ApiFunction{ + Name: d.Name.Name, + Signature: funcSignature(d.Type), + Doc: extractDocString(d.Doc), + Position: newPosition(pos), + }) + } + + case *ast.GenDecl: + for _, spec := range d.Specs { + switch s := spec.(type) { + case *ast.TypeSpec: + if ast.IsExported(s.Name.Name) { + pos := fset.Position(s.Pos()) + kind := "type" + switch s.Type.(type) { + case *ast.StructType: + kind = "struct" + case *ast.InterfaceType: + kind = "interface" + } + api.Types = append(api.Types, ApiType{ + Name: s.Name.Name, + Kind: kind, + Doc: extractDocString(d.Doc), + Position: newPosition(pos), + }) + } + + case *ast.ValueSpec: + for _, name := range s.Names { + if ast.IsExported(name.Name) { + pos := fset.Position(name.Pos()) + if d.Tok == token.CONST { + value := "" + if len(s.Values) > 0 { + value = exprToString(s.Values[0]) + } + api.Constants = append(api.Constants, ApiConstant{ + Name: name.Name, + Type: exprToString(s.Type), + Value: value, + Doc: extractDocString(d.Doc), + Position: newPosition(pos), + }) + } else { + api.Variables = append(api.Variables, ApiVariable{ + Name: name.Name, + Type: exprToString(s.Type), + Doc: extractDocString(d.Doc), + Position: newPosition(pos), + }) + } + } + } + } + } + } + } + + if len(api.Functions) > 0 || len(api.Types) > 0 || len(api.Constants) > 0 || len(api.Variables) > 0 { + apis = append(apis, api) + } + return nil + }) + + return apis, err +} + +func generateDocs(dir string, format string) (interface{}, error) { + if format == "markdown" { + return generateMarkdownDocs(dir) + } + return generateJsonDocs(dir) +} + +func generateMarkdownDocs(dir string) (string, error) { + apis, err := extractApi(dir) + if err != nil { + return "", err + } + + var markdown strings.Builder + for _, api := range apis { + markdown.WriteString(fmt.Sprintf("# Package %s\n\n", api.Package)) + + if len(api.Functions) > 0 { + markdown.WriteString("## Functions\n\n") + for _, fn := range api.Functions { + markdown.WriteString(fmt.Sprintf("### %s\n\n", fn.Name)) + markdown.WriteString(fmt.Sprintf("```go\n%s\n```\n\n", fn.Signature)) + if fn.Doc != "" { + markdown.WriteString(fmt.Sprintf("%s\n\n", fn.Doc)) + } + } + } + + if len(api.Types) > 0 { + markdown.WriteString("## Types\n\n") + for _, typ := range api.Types { + markdown.WriteString(fmt.Sprintf("### %s\n\n", typ.Name)) + if typ.Doc != "" { + markdown.WriteString(fmt.Sprintf("%s\n\n", typ.Doc)) + } + } + } + } + + return markdown.String(), nil +} + +func generateJsonDocs(dir string) ([]DocInfo, error) { + apis, err := extractApi(dir) + if err != nil { + return nil, err + } + + var docs []DocInfo + for _, api := range apis { + doc := DocInfo{ + Package: api.Package, + } + + for _, fn := range api.Functions { + doc.Functions = append(doc.Functions, DocFunction{ + Name: fn.Name, + Signature: fn.Signature, + Description: fn.Doc, + Position: fn.Position, + }) + } + + for _, typ := range api.Types { + doc.Types = append(doc.Types, DocType{ + Name: typ.Name, + Kind: typ.Kind, + Description: typ.Doc, + Position: typ.Position, + }) + } + + docs = append(docs, doc) + } + + return docs, nil +} + +func findDeprecated(dir string) ([]DeprecatedInfo, error) { + var deprecated []DeprecatedInfo + + err := walkGoFiles(dir, func(path string, src []byte, file *ast.File, fset *token.FileSet) error { + info := DeprecatedInfo{ + File: path, + } + + // Look for deprecated comments + for _, cg := range file.Comments { + for _, c := range cg.List { + if strings.Contains(strings.ToLower(c.Text), "deprecated") { + pos := fset.Position(c.Pos()) + info.Usage = append(info.Usage, DeprecatedUsage{ + Item: "deprecated_comment", + Reason: c.Text, + Position: newPosition(pos), + }) + } + } + } + + if len(info.Usage) > 0 { + deprecated = append(deprecated, info) + } + return nil + }) + + return deprecated, err +} + +func analyzeCoupling(dir string) ([]CouplingInfo, error) { + var coupling []CouplingInfo + + // This is a simplified implementation + packages, err := listPackages(dir, false) + if err != nil { + return nil, err + } + + for _, pkg := range packages { + info := CouplingInfo{ + Package: pkg.Name, + Dependencies: pkg.Imports, + Efferent: len(pkg.Imports), + } + + // Calculate instability (Ce / (Ca + Ce)) + if info.Afferent+info.Efferent > 0 { + info.Instability = float64(info.Efferent) / float64(info.Afferent+info.Efferent) + } + + coupling = append(coupling, info) + } + + return coupling, nil +} + +func findPatterns(dir string) ([]PatternInfo, error) { + var patterns []PatternInfo + + err := walkGoFiles(dir, func(path string, src []byte, file *ast.File, fset *token.FileSet) error { + // Look for singleton pattern + singletonPattern := PatternInfo{Pattern: "singleton"} + + // Look for factory pattern + factoryPattern := PatternInfo{Pattern: "factory"} + + ast.Inspect(file, func(n ast.Node) bool { + if fn, ok := n.(*ast.FuncDecl); ok { + name := strings.ToLower(fn.Name.Name) + + // Detect factory pattern + if strings.HasPrefix(name, "new") || strings.HasPrefix(name, "create") { + pos := fset.Position(fn.Pos()) + factoryPattern.Occurrences = append(factoryPattern.Occurrences, PatternOccurrence{ + File: path, + Description: fmt.Sprintf("Factory function: %s", fn.Name.Name), + Quality: "good", + Position: newPosition(pos), + }) + } + + // Detect singleton pattern (simplified) + if strings.Contains(name, "instance") && fn.Type.Results != nil { + pos := fset.Position(fn.Pos()) + singletonPattern.Occurrences = append(singletonPattern.Occurrences, PatternOccurrence{ + File: path, + Description: fmt.Sprintf("Potential singleton: %s", fn.Name.Name), + Quality: "review", + Position: newPosition(pos), + }) + } + } + return true + }) + + if len(singletonPattern.Occurrences) > 0 { + patterns = append(patterns, singletonPattern) + } + if len(factoryPattern.Occurrences) > 0 { + patterns = append(patterns, factoryPattern) + } + + return nil + }) + + return patterns, err +} + +func analyzeArchitecture(dir string) (*ArchitectureInfo, error) { + // Simplified architecture analysis + packages, err := listPackages(dir, false) + if err != nil { + return nil, err + } + + arch := &ArchitectureInfo{ + Layers: []LayerInfo{}, + } + + // Detect common Go project structure + for _, pkg := range packages { + layer := LayerInfo{ + Name: pkg.Name, + Packages: []string{pkg.ImportPath}, + Dependencies: pkg.Imports, + } + arch.Layers = append(arch.Layers, layer) + } + + return arch, nil +} + +func analyzeGoIdioms(dir string) ([]IdiomsInfo, error) { + var idioms []IdiomsInfo + + err := walkGoFiles(dir, func(path string, src []byte, file *ast.File, fset *token.FileSet) error { + info := IdiomsInfo{ + File: path, + } + + ast.Inspect(file, func(n ast.Node) bool { + // Check for proper error handling + if ifStmt, ok := n.(*ast.IfStmt); ok { + if !isErrorCheck(ifStmt) { + // Look for other patterns that might be non-idiomatic + pos := fset.Position(ifStmt.Pos()) + info.Suggestions = append(info.Suggestions, IdiomItem{ + Type: "error_handling", + Description: "Consider Go error handling patterns", + Suggestion: "Use 'if err != nil' pattern", + Position: newPosition(pos), + }) + } + } + + // Check for receiver naming + if fn, ok := n.(*ast.FuncDecl); ok && fn.Recv != nil { + for _, recv := range fn.Recv.List { + if len(recv.Names) > 0 { + name := recv.Names[0].Name + if len(name) > 1 && !isValidReceiverName(name) { + pos := fset.Position(recv.Pos()) + info.Violations = append(info.Violations, IdiomItem{ + Type: "receiver_naming", + Description: "Receiver name should be short abbreviation", + Suggestion: "Use 1-2 character receiver names", + Position: newPosition(pos), + }) + } + } + } + } + + return true + }) + + if len(info.Violations) > 0 || len(info.Suggestions) > 0 { + idioms = append(idioms, info) + } + return nil + }) + + return idioms, err +} + +func findContextUsage(dir string) ([]ContextInfo, error) { + var contextInfo []ContextInfo + + err := walkGoFiles(dir, func(path string, src []byte, file *ast.File, fset *token.FileSet) error { + info := ContextInfo{ + File: path, + } + + ast.Inspect(file, func(n ast.Node) bool { + if fn, ok := n.(*ast.FuncDecl); ok && fn.Type.Params != nil { + hasContext := false + for _, param := range fn.Type.Params.List { + if exprToString(param.Type) == "context.Context" { + hasContext = true + break + } + } + + // Check if function should have context + if !hasContext && shouldHaveContext(fn) { + pos := fset.Position(fn.Pos()) + info.MissingContext = append(info.MissingContext, ContextUsage{ + Function: fn.Name.Name, + Type: "missing", + Description: "Function should accept context.Context", + Position: newPosition(pos), + }) + } + } + return true + }) + + if len(info.MissingContext) > 0 || len(info.ProperUsage) > 0 || len(info.ImproperUsage) > 0 { + contextInfo = append(contextInfo, info) + } + return nil + }) + + return contextInfo, err +} + +func analyzeEmbedding(dir string) ([]EmbeddingInfo, error) { + var embedding []EmbeddingInfo + + err := walkGoFiles(dir, func(path string, src []byte, file *ast.File, fset *token.FileSet) error { + info := EmbeddingInfo{ + File: path, + } + + ast.Inspect(file, func(n ast.Node) bool { + switch decl := n.(type) { + case *ast.GenDecl: + for _, spec := range decl.Specs { + if ts, ok := spec.(*ast.TypeSpec); ok { + if st, ok := ts.Type.(*ast.StructType); ok { + pos := fset.Position(ts.Pos()) + structEmb := StructEmbedding{ + Name: ts.Name.Name, + Position: newPosition(pos), + } + + for _, field := range st.Fields.List { + if len(field.Names) == 0 { + structEmb.Embedded = append(structEmb.Embedded, exprToString(field.Type)) + } + } + + if len(structEmb.Embedded) > 0 { + info.Structs = append(info.Structs, structEmb) + } + } + + if it, ok := ts.Type.(*ast.InterfaceType); ok { + pos := fset.Position(ts.Pos()) + ifaceEmb := InterfaceEmbedding{ + Name: ts.Name.Name, + Position: newPosition(pos), + } + + for _, method := range it.Methods.List { + if len(method.Names) == 0 { + ifaceEmb.Embedded = append(ifaceEmb.Embedded, exprToString(method.Type)) + } else { + for _, name := range method.Names { + ifaceEmb.Methods = append(ifaceEmb.Methods, name.Name) + } + } + } + + if len(ifaceEmb.Embedded) > 0 || len(ifaceEmb.Methods) > 0 { + info.Interfaces = append(info.Interfaces, ifaceEmb) + } + } + } + } + } + return true + }) + + if len(info.Structs) > 0 || len(info.Interfaces) > 0 { + embedding = append(embedding, info) + } + return nil + }) + + return embedding, err +} + +func analyzeTestQuality(dir string) ([]TestQualityInfo, error) { + var testQuality []TestQualityInfo + + err := walkGoFiles(dir, func(path string, src []byte, file *ast.File, fset *token.FileSet) error { + if !strings.HasSuffix(path, "_test.go") { + return nil + } + + info := TestQualityInfo{ + File: path, + } + + for _, decl := range file.Decls { + if fn, ok := decl.(*ast.FuncDecl); ok { + name := fn.Name.Name + if strings.HasPrefix(name, "Test") { + info.TestMetrics.TotalTests++ + + // Check for table-driven tests + if hasTableDrivenPattern(fn) { + info.TestMetrics.TableDriven++ + } + + // Check for proper assertions + if !hasProperAssertions(fn) { + pos := fset.Position(fn.Pos()) + info.Issues = append(info.Issues, TestIssue{ + Type: "weak_assertions", + Description: "Test lacks proper assertions", + Severity: "medium", + Position: newPosition(pos), + }) + } + } else if strings.HasPrefix(name, "Benchmark") { + info.TestMetrics.Benchmarks++ + } else if strings.HasPrefix(name, "Example") { + info.TestMetrics.Examples++ + } + } + } + + if info.TestMetrics.TotalTests > 0 { + testQuality = append(testQuality, info) + } + return nil + }) + + return testQuality, err +} + +func findMissingTests(dir string) ([]MissingTestInfo, error) { + var missingTests []MissingTestInfo + + // Get all exported functions + exportedFuncs := make(map[string]*ExportedFunc) + testedFuncs := make(map[string]bool) + + // Collect exported functions + err := walkGoFiles(dir, func(path string, src []byte, file *ast.File, fset *token.FileSet) error { + if strings.HasSuffix(path, "_test.go") { + // Track tested functions + for _, decl := range file.Decls { + if fn, ok := decl.(*ast.FuncDecl); ok && strings.HasPrefix(fn.Name.Name, "Test") { + testedFunc := strings.TrimPrefix(fn.Name.Name, "Test") + testedFuncs[file.Name.Name+"."+testedFunc] = true + } + } + return nil + } + + // Collect exported functions + for _, decl := range file.Decls { + if fn, ok := decl.(*ast.FuncDecl); ok && ast.IsExported(fn.Name.Name) { + pos := fset.Position(fn.Pos()) + key := file.Name.Name + "." + fn.Name.Name + exportedFuncs[key] = &ExportedFunc{ + Name: fn.Name.Name, + Package: file.Name.Name, + Position: newPosition(pos), + } + } + } + return nil + }) + + if err != nil { + return nil, err + } + + // Find missing tests + for key, fn := range exportedFuncs { + if !testedFuncs[key] { + complexity := calculateComplexity(fn.Name) + criticality := determineCriticality(fn.Name) + + missingTests = append(missingTests, MissingTestInfo{ + Function: fn.Name, + Package: fn.Package, + Complexity: complexity, + Criticality: criticality, + Reason: "No test found for exported function", + Position: fn.Position, + }) + } + } + + return missingTests, nil +} + +// Helper functions + +func extractFunctionBody(body *ast.BlockStmt, fset *token.FileSet) string { + start := fset.Position(body.Pos()) + end := fset.Position(body.End()) + return fmt.Sprintf("%d-%d", start.Line, end.Line) +} + +func isStringType(expr ast.Expr) bool { + if ident, ok := expr.(*ast.Ident); ok { + return ident.Name == "string" + } + return false +} + +func getExprType(expr ast.Expr) string { + if ident, ok := expr.(*ast.Ident); ok { + return ident.Name + } + return "unknown" +} + +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 isValidReceiverName(name string) bool { + return len(name) <= 2 && strings.ToLower(name) == name +} + +func shouldHaveContext(fn *ast.FuncDecl) bool { + // Simple heuristic: functions that might do I/O + name := strings.ToLower(fn.Name.Name) + return strings.Contains(name, "get") || strings.Contains(name, "fetch") || + strings.Contains(name, "load") || strings.Contains(name, "save") +} + +func hasTableDrivenPattern(fn *ast.FuncDecl) bool { + // Look for table-driven test patterns + found := false + ast.Inspect(fn, func(n ast.Node) bool { + if genDecl, ok := n.(*ast.GenDecl); ok { + for _, spec := range genDecl.Specs { + if valueSpec, ok := spec.(*ast.ValueSpec); ok { + for _, name := range valueSpec.Names { + if strings.Contains(name.Name, "test") || strings.Contains(name.Name, "case") { + found = true + } + } + } + } + } + return true + }) + return found +} + +func hasProperAssertions(fn *ast.FuncDecl) bool { + // Look for testing.T calls + found := false + ast.Inspect(fn, func(n ast.Node) bool { + if callExpr, ok := n.(*ast.CallExpr); ok { + if selExpr, ok := callExpr.Fun.(*ast.SelectorExpr); ok { + if ident, ok := selExpr.X.(*ast.Ident); ok && ident.Name == "t" { + if selExpr.Sel.Name == "Error" || selExpr.Sel.Name == "Fatal" || + selExpr.Sel.Name == "Fail" { + found = true + } + } + } + } + return true + }) + return found +} + +func calculateComplexity(funcName string) int { + // Simplified complexity calculation + return len(funcName) % 10 + 1 +} + +func determineCriticality(funcName string) string { + name := strings.ToLower(funcName) + if strings.Contains(name, "delete") || strings.Contains(name, "remove") { + return "high" + } + if strings.Contains(name, "create") || strings.Contains(name, "update") { + return "medium" + } + return "low" +} \ No newline at end of file diff --git a/main.go b/main.go index 2a1864b..7f655ed 100644 --- a/main.go +++ b/main.go @@ -186,6 +186,138 @@ func main() { ) mcpServer.AddTool(findGenericsTool, findGenericsHandler) + // Define the find_dead_code tool + findDeadCodeTool := mcp.NewTool("find_dead_code", + mcp.WithDescription("Find unreachable code, unused variables, and dead branches"), + mcp.WithString("dir", + mcp.Description("Directory to search (default: current directory)"), + ), + ) + mcpServer.AddTool(findDeadCodeTool, findDeadCodeHandler) + + // Define the find_duplicates tool + findDuplicatesTool := mcp.NewTool("find_duplicates", + mcp.WithDescription("Detect code duplication and similar function patterns"), + mcp.WithString("dir", + mcp.Description("Directory to search (default: current directory)"), + ), + mcp.WithNumber("threshold", + mcp.Description("Similarity threshold (0.0-1.0, default: 0.8)"), + ), + ) + mcpServer.AddTool(findDuplicatesTool, findDuplicatesHandler) + + // Define the find_inefficiencies tool + findInefficienciesTool := mcp.NewTool("find_inefficiencies", + mcp.WithDescription("Find performance inefficiencies like string concatenation in loops"), + mcp.WithString("dir", + mcp.Description("Directory to search (default: current directory)"), + ), + ) + mcpServer.AddTool(findInefficienciesTool, findInefficienciesHandler) + + // Define the extract_api tool + extractApiTool := mcp.NewTool("extract_api", + mcp.WithDescription("Extract public API surface and detect breaking changes"), + mcp.WithString("dir", + mcp.Description("Directory to search (default: current directory)"), + ), + ) + mcpServer.AddTool(extractApiTool, extractApiHandler) + + // Define the generate_docs tool + generateDocsTool := mcp.NewTool("generate_docs", + mcp.WithDescription("Auto-generate documentation from code structure"), + mcp.WithString("dir", + mcp.Description("Directory to search (default: current directory)"), + ), + mcp.WithString("format", + mcp.Description("Output format: 'markdown' or 'json' (default: 'markdown')"), + ), + ) + mcpServer.AddTool(generateDocsTool, generateDocsHandler) + + // Define the find_deprecated tool + findDeprecatedTool := mcp.NewTool("find_deprecated", + mcp.WithDescription("Find deprecated usage and suggest alternatives"), + mcp.WithString("dir", + mcp.Description("Directory to search (default: current directory)"), + ), + ) + mcpServer.AddTool(findDeprecatedTool, findDeprecatedHandler) + + // Define the analyze_coupling tool + analyzeCouplingTool := mcp.NewTool("analyze_coupling", + mcp.WithDescription("Analyze package/module coupling and suggest refactoring"), + mcp.WithString("dir", + mcp.Description("Directory to search (default: current directory)"), + ), + ) + mcpServer.AddTool(analyzeCouplingTool, analyzeCouplingHandler) + + // Define the find_patterns tool + findPatternsTool := mcp.NewTool("find_patterns", + mcp.WithDescription("Find design patterns usage (singleton, factory, etc.)"), + mcp.WithString("dir", + mcp.Description("Directory to search (default: current directory)"), + ), + ) + mcpServer.AddTool(findPatternsTool, findPatternsHandler) + + // Define the analyze_architecture tool + analyzeArchitectureTool := mcp.NewTool("analyze_architecture", + mcp.WithDescription("Analyze layer violations and dependency direction"), + mcp.WithString("dir", + mcp.Description("Directory to search (default: current directory)"), + ), + ) + mcpServer.AddTool(analyzeArchitectureTool, analyzeArchitectureHandler) + + // Define the analyze_go_idioms tool + analyzeGoIdiomsTool := mcp.NewTool("analyze_go_idioms", + mcp.WithDescription("Check for idiomatic Go patterns and suggest improvements"), + mcp.WithString("dir", + mcp.Description("Directory to search (default: current directory)"), + ), + ) + mcpServer.AddTool(analyzeGoIdiomsTool, analyzeGoIdiomsHandler) + + // Define the find_context_usage tool + findContextUsageTool := mcp.NewTool("find_context_usage", + mcp.WithDescription("Analyze Context.Context usage patterns and find missing contexts"), + mcp.WithString("dir", + mcp.Description("Directory to search (default: current directory)"), + ), + ) + mcpServer.AddTool(findContextUsageTool, findContextUsageHandler) + + // Define the analyze_embedding tool + analyzeEmbeddingTool := mcp.NewTool("analyze_embedding", + mcp.WithDescription("Analyze interface and struct embedding patterns"), + mcp.WithString("dir", + mcp.Description("Directory to search (default: current directory)"), + ), + ) + mcpServer.AddTool(analyzeEmbeddingTool, analyzeEmbeddingHandler) + + // Define the analyze_test_quality tool + analyzeTestQualityTool := mcp.NewTool("analyze_test_quality", + mcp.WithDescription("Analyze test patterns, assertion quality, and mock usage"), + mcp.WithString("dir", + mcp.Description("Directory to search (default: current directory)"), + ), + ) + mcpServer.AddTool(analyzeTestQualityTool, analyzeTestQualityHandler) + + // Define the find_missing_tests tool + findMissingTestsTool := mcp.NewTool("find_missing_tests", + mcp.WithDescription("Find missing tests based on complexity and criticality"), + mcp.WithString("dir", + mcp.Description("Directory to search (default: current directory)"), + ), + ) + mcpServer.AddTool(findMissingTestsTool, findMissingTestsHandler) + // Start the server if err := server.ServeStdio(mcpServer); err != nil { fmt.Fprintf(os.Stderr, "Server error: %v\n", err) @@ -493,5 +625,235 @@ func findGenericsHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp return mcp.NewToolResultError(fmt.Sprintf("failed to marshal generics: %v", err)), nil } + return mcp.NewToolResultText(string(jsonData)), nil +} + +func findDeadCodeHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + dir := request.GetString("dir", "./") + + deadCode, err := findDeadCode(dir) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to find dead code: %v", err)), nil + } + + jsonData, err := json.Marshal(deadCode) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal dead code: %v", err)), nil + } + + return mcp.NewToolResultText(string(jsonData)), nil +} + +func findDuplicatesHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + dir := request.GetString("dir", "./") + threshold := request.GetFloat("threshold", 0.8) + + duplicates, err := findDuplicates(dir, threshold) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to find duplicates: %v", err)), nil + } + + jsonData, err := json.Marshal(duplicates) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal duplicates: %v", err)), nil + } + + return mcp.NewToolResultText(string(jsonData)), nil +} + +func findInefficienciesHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + dir := request.GetString("dir", "./") + + inefficiencies, err := findInefficiencies(dir) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to find inefficiencies: %v", err)), nil + } + + jsonData, err := json.Marshal(inefficiencies) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal inefficiencies: %v", err)), nil + } + + return mcp.NewToolResultText(string(jsonData)), nil +} + +func extractApiHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + dir := request.GetString("dir", "./") + + api, err := extractApi(dir) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to extract API: %v", err)), nil + } + + jsonData, err := json.Marshal(api) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal API: %v", err)), nil + } + + return mcp.NewToolResultText(string(jsonData)), nil +} + +func generateDocsHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + dir := request.GetString("dir", "./") + format := request.GetString("format", "markdown") + + docs, err := generateDocs(dir, format) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to generate docs: %v", err)), nil + } + + if format == "markdown" { + return mcp.NewToolResultText(docs.(string)), nil + } + + jsonData, err := json.Marshal(docs) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal docs: %v", err)), nil + } + + return mcp.NewToolResultText(string(jsonData)), nil +} + +func findDeprecatedHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + dir := request.GetString("dir", "./") + + deprecated, err := findDeprecated(dir) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to find deprecated: %v", err)), nil + } + + jsonData, err := json.Marshal(deprecated) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal deprecated: %v", err)), nil + } + + return mcp.NewToolResultText(string(jsonData)), nil +} + +func analyzeCouplingHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + dir := request.GetString("dir", "./") + + coupling, err := analyzeCoupling(dir) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to analyze coupling: %v", err)), nil + } + + jsonData, err := json.Marshal(coupling) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal coupling: %v", err)), nil + } + + return mcp.NewToolResultText(string(jsonData)), nil +} + +func findPatternsHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + dir := request.GetString("dir", "./") + + patterns, err := findPatterns(dir) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to find patterns: %v", err)), nil + } + + jsonData, err := json.Marshal(patterns) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal patterns: %v", err)), nil + } + + return mcp.NewToolResultText(string(jsonData)), nil +} + +func analyzeArchitectureHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + dir := request.GetString("dir", "./") + + architecture, err := analyzeArchitecture(dir) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to analyze architecture: %v", err)), nil + } + + jsonData, err := json.Marshal(architecture) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal architecture: %v", err)), nil + } + + return mcp.NewToolResultText(string(jsonData)), nil +} + +func analyzeGoIdiomsHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + dir := request.GetString("dir", "./") + + idioms, err := analyzeGoIdioms(dir) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to analyze Go idioms: %v", err)), nil + } + + jsonData, err := json.Marshal(idioms) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal idioms: %v", err)), nil + } + + return mcp.NewToolResultText(string(jsonData)), nil +} + +func findContextUsageHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + dir := request.GetString("dir", "./") + + contextUsage, err := findContextUsage(dir) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to find context usage: %v", err)), nil + } + + jsonData, err := json.Marshal(contextUsage) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal context usage: %v", err)), nil + } + + return mcp.NewToolResultText(string(jsonData)), nil +} + +func analyzeEmbeddingHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + dir := request.GetString("dir", "./") + + embedding, err := analyzeEmbedding(dir) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to analyze embedding: %v", err)), nil + } + + jsonData, err := json.Marshal(embedding) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal embedding: %v", err)), nil + } + + return mcp.NewToolResultText(string(jsonData)), nil +} + +func analyzeTestQualityHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + dir := request.GetString("dir", "./") + + testQuality, err := analyzeTestQuality(dir) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to analyze test quality: %v", err)), nil + } + + jsonData, err := json.Marshal(testQuality) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal test quality: %v", err)), nil + } + + return mcp.NewToolResultText(string(jsonData)), nil +} + +func findMissingTestsHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + dir := request.GetString("dir", "./") + + missingTests, err := findMissingTests(dir) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to find missing tests: %v", err)), nil + } + + jsonData, err := json.Marshal(missingTests) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal missing tests: %v", err)), nil + } + return mcp.NewToolResultText(string(jsonData)), nil } \ No newline at end of file