mirror of
https://github.com/Dvorinka/Devour.git
synced 2026-06-04 04:23:02 +00:00
first commit
This commit is contained in:
@@ -0,0 +1,363 @@
|
||||
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())
|
||||
}
|
||||
Reference in New Issue
Block a user