mirror of
https://github.com/Dvorinka/Devour.git
synced 2026-06-04 20:43:05 +00:00
364 lines
8.3 KiB
Go
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())
|
|
}
|