1121 lines
29 KiB
Go
1121 lines
29 KiB
Go
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"
|
|
} |