Files
Devour/internal/quality/plugins/go/plugin.go
T
Tomas Dvorak 55885a0e8f first commit
2026-02-22 10:42:17 +01:00

364 lines
8.3 KiB
Go

package goplugin
import (
"context"
"fmt"
"go/ast"
"go/parser"
"go/token"
"go/types"
"os"
"path/filepath"
"strings"
"github.com/yourorg/devour/internal/quality"
"github.com/yourorg/devour/internal/quality/plugins"
"github.com/yourorg/devour/internal/quality/plugins/go/analyzers"
"github.com/yourorg/devour/internal/quality/plugins/go/fixers"
"golang.org/x/tools/go/packages"
)
type GoPlugin struct{}
func New() *GoPlugin {
return &GoPlugin{}
}
func (p *GoPlugin) Name() string {
return "go"
}
func (p *GoPlugin) Extensions() []string {
return []string{".go"}
}
func (p *GoPlugin) MarkerFiles() []string {
return []string{"go.mod", "go.sum"}
}
func (p *GoPlugin) DefaultSrcDir() string {
return "."
}
func (p *GoPlugin) CreateDetectors(finder quality.FileFinder) []quality.Detector {
return []quality.Detector{
analyzers.NewDeadCodeDetector(finder),
analyzers.NewEnhancedDeadCodeDetector(finder),
analyzers.NewUnusedImportDetector(finder),
analyzers.NewCycleDetector(finder),
analyzers.NewSecurityDetector(finder),
analyzers.NewComplexityASTDetector(finder),
analyzers.NewLargeFileDetector(finder),
analyzers.NewGodStructDetector(finder),
analyzers.NewGodFunctionDetector(finder),
analyzers.NewDebugLogDetector(finder),
analyzers.NewSingleUseDetector(finder),
analyzers.NewCouplingDetector(finder),
analyzers.NewTestCoverageDetector(finder),
analyzers.NewUntestedFuncDetector(finder),
analyzers.NewOrphanedFileDetector(finder),
analyzers.NewDeprecatedUsageDetector(finder),
}
}
func (p *GoPlugin) CreateFixers() []plugins.Fixer {
return []plugins.Fixer{
fixers.NewUnusedImportFixer(),
fixers.NewFormattingFixer(),
fixers.NewDeadCodeFixer(),
fixers.NewComplexityHintFixer(),
fixers.NewIoutilFixer(),
fixers.NewDocCommentFixer(),
}
}
func (p *GoPlugin) AnalyzeFile(ctx context.Context, path string, config *quality.Config) (*plugins.FileAnalysis, error) {
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, path, nil, parser.ParseComments|parser.AllErrors)
if err != nil {
return nil, fmt.Errorf("parse error: %w", err)
}
analysis := &plugins.FileAnalysis{
Path: path,
Package: node.Name.Name,
LOC: countLOC(path),
}
analysis.Imports = p.extractImports(node, fset)
analysis.Functions = p.extractFunctions(node, path, fset)
analysis.Types = p.extractTypes(node, path, fset)
analysis.Variables = p.extractVariables(node, path, fset)
analysis.Comments = p.extractComments(node, path, fset)
return analysis, nil
}
func (p *GoPlugin) BuildDependencyGraph(ctx context.Context, rootPath string) (*plugins.DependencyGraph, error) {
cfg := &packages.Config{
Mode: packages.NeedName | packages.NeedImports | packages.NeedFiles,
Dir: rootPath,
}
pkgs, err := packages.Load(cfg, "./...")
if err != nil {
return nil, fmt.Errorf("failed to load packages: %w", err)
}
graph := &plugins.DependencyGraph{
Packages: make(map[string]*plugins.PackageNode),
Edges: []plugins.DependencyEdge{},
}
for _, pkg := range pkgs {
node := &plugins.PackageNode{
Name: pkg.Name,
Path: pkg.PkgPath,
Files: pkg.GoFiles,
IsLocal: true,
}
for _, imp := range pkg.Imports {
node.Imports = append(node.Imports, imp.PkgPath)
graph.Edges = append(graph.Edges, plugins.DependencyEdge{
From: pkg.PkgPath,
To: imp.PkgPath,
Type: plugins.EdgeTypeImport,
})
}
graph.Packages[pkg.PkgPath] = node
}
graph.Cycles = p.detectCycles(graph)
return graph, nil
}
func (p *GoPlugin) extractImports(node *ast.File, fset *token.FileSet) []plugins.ImportInfo {
var imports []plugins.ImportInfo
for _, imp := range node.Imports {
info := plugins.ImportInfo{
Path: strings.Trim(imp.Path.Value, `"`),
Line: fset.Position(imp.Pos()).Line,
}
if imp.Name != nil {
info.Alias = imp.Name.Name
}
imports = append(imports, info)
}
return imports
}
func (p *GoPlugin) extractFunctions(node *ast.File, path string, fset *token.FileSet) []quality.FunctionInfo {
var functions []quality.FunctionInfo
for _, decl := range node.Decls {
fn, ok := decl.(*ast.FuncDecl)
if !ok {
continue
}
info := quality.FunctionInfo{
Name: fn.Name.Name,
File: path,
Line: fset.Position(fn.Pos()).Line,
EndLine: fset.Position(fn.End()).Line,
}
info.LOC = info.EndLine - info.Line + 1
var params []string
if fn.Type.Params != nil {
for _, field := range fn.Type.Params.List {
for _, name := range field.Names {
params = append(params, name.Name)
}
}
}
info.Params = params
if fn.Type.Results != nil {
info.ReturnAnnotation = fmt.Sprintf("%v", fn.Type.Results)
}
functions = append(functions, info)
}
return functions
}
func (p *GoPlugin) extractTypes(node *ast.File, path string, fset *token.FileSet) []plugins.TypeInfo {
var typeInfos []plugins.TypeInfo
for _, decl := range node.Decls {
gen, ok := decl.(*ast.GenDecl)
if !ok || gen.Tok != token.TYPE {
continue
}
for _, spec := range gen.Specs {
typeSpec, ok := spec.(*ast.TypeSpec)
if !ok {
continue
}
info := plugins.TypeInfo{
Name: typeSpec.Name.Name,
File: path,
Line: fset.Position(typeSpec.Pos()).Line,
IsExported: ast.IsExported(typeSpec.Name.Name),
}
switch t := typeSpec.Type.(type) {
case *ast.StructType:
info.Underlying = "struct"
case *ast.InterfaceType:
info.Underlying = "interface"
default:
info.Underlying = fmt.Sprintf("%T", t)
}
typeInfos = append(typeInfos, info)
}
}
return typeInfos
}
func (p *GoPlugin) extractVariables(node *ast.File, path string, fset *token.FileSet) []plugins.VariableInfo {
var variables []plugins.VariableInfo
for _, decl := range node.Decls {
gen, ok := decl.(*ast.GenDecl)
if !ok || (gen.Tok != token.VAR && gen.Tok != token.CONST) {
continue
}
for _, spec := range gen.Specs {
valueSpec, ok := spec.(*ast.ValueSpec)
if !ok {
continue
}
for _, name := range valueSpec.Names {
info := plugins.VariableInfo{
Name: name.Name,
File: path,
Line: fset.Position(name.Pos()).Line,
IsExported: ast.IsExported(name.Name),
}
if valueSpec.Type != nil {
info.Type = fmt.Sprintf("%v", valueSpec.Type)
}
variables = append(variables, info)
}
}
}
return variables
}
func (p *GoPlugin) extractComments(node *ast.File, path string, fset *token.FileSet) []plugins.CommentInfo {
var comments []plugins.CommentInfo
for _, group := range node.Comments {
for _, comment := range group.List {
info := plugins.CommentInfo{
Text: comment.Text,
File: path,
Line: fset.Position(comment.Pos()).Line,
IsDoc: strings.HasPrefix(comment.Text, "//"),
}
comments = append(comments, info)
}
}
return comments
}
func (p *GoPlugin) detectCycles(graph *plugins.DependencyGraph) [][]string {
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)
node, exists := graph.Packages[pkg]
if !exists {
return false
}
for _, imp := range node.Imports {
if !visited[imp] {
if dfs(imp) {
return true
}
} else if recStack[imp] {
cycleStart := -1
for i, p := range path {
if p == imp {
cycleStart = i
break
}
}
if cycleStart >= 0 {
cycle := make([]string, len(path)-cycleStart)
copy(cycle, path[cycleStart:])
cycles = append(cycles, cycle)
}
}
}
path = path[:len(path)-1]
recStack[pkg] = false
return false
}
for pkg := range graph.Packages {
if !visited[pkg] {
dfs(pkg)
}
}
return cycles
}
func (p *GoPlugin) LoadTypesInfo(ctx context.Context, path string) (*types.Info, *token.FileSet, error) {
cfg := &packages.Config{
Mode: packages.NeedName | packages.NeedTypes | packages.NeedTypesInfo,
Dir: filepath.Dir(path),
}
pkgs, err := packages.Load(cfg, filepath.Base(path))
if err != nil {
return nil, nil, err
}
if len(pkgs) == 0 {
return nil, nil, fmt.Errorf("no packages found")
}
return pkgs[0].TypesInfo, pkgs[0].Fset, nil
}
func countLOC(path string) int {
data, err := os.ReadFile(path)
if err != nil {
return 0
}
return strings.Count(string(data), "\n") + 1
}
func init() {
plugins.Register(New())
}