woodpecker/vendor/github.com/fzipp/gocyclo/analyze.go

152 lines
3.7 KiB
Go
Raw Normal View History

// Copyright 2020 Frederik Zipp. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gocyclo
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"log"
"os"
"path/filepath"
"regexp"
"strings"
)
// Analyze calculates the cyclomatic complexities of the functions and methods
// in the Go source code files in the given paths. If a path is a directory
// all Go files under that directory are analyzed recursively.
// Files with paths matching the 'ignore' regular expressions are skipped.
// The 'ignore' parameter can be nil, meaning that no files are skipped.
func Analyze(paths []string, ignore *regexp.Regexp) Stats {
var stats Stats
for _, path := range paths {
info, err := os.Stat(path)
if err != nil {
log.Printf("could not get file info for path %q: %s\n", path, err)
continue
}
if info.IsDir() {
stats = analyzeDir(path, ignore, stats)
} else {
stats = analyzeFile(path, ignore, stats)
}
}
return stats
}
func analyzeDir(dirname string, ignore *regexp.Regexp, stats Stats) Stats {
filepath.Walk(dirname, func(path string, info os.FileInfo, err error) error {
if err == nil && isGoFile(info) {
stats = analyzeFile(path, ignore, stats)
}
return err
})
return stats
}
func isGoFile(f os.FileInfo) bool {
return !f.IsDir() && strings.HasSuffix(f.Name(), ".go")
}
func analyzeFile(path string, ignore *regexp.Regexp, stats Stats) Stats {
if isIgnored(path, ignore) {
return stats
}
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
if err != nil {
log.Fatal(err)
}
return AnalyzeASTFile(f, fset, stats)
}
func isIgnored(path string, ignore *regexp.Regexp) bool {
return ignore != nil && ignore.MatchString(path)
}
// AnalyzeASTFile calculates the cyclomatic complexities of the functions
// and methods in the abstract syntax tree (AST) of a parsed Go file and
// appends the results to the given Stats slice.
func AnalyzeASTFile(f *ast.File, fs *token.FileSet, s Stats) Stats {
analyzer := &fileAnalyzer{
file: f,
fileSet: fs,
stats: s,
}
return analyzer.analyze()
}
type fileAnalyzer struct {
file *ast.File
fileSet *token.FileSet
stats Stats
}
func (a *fileAnalyzer) analyze() Stats {
for _, decl := range a.file.Decls {
a.analyzeDecl(decl)
}
return a.stats
}
func (a *fileAnalyzer) analyzeDecl(d ast.Decl) {
switch decl := d.(type) {
case *ast.FuncDecl:
a.addStatIfNotIgnored(decl, funcName(decl), decl.Doc)
case *ast.GenDecl:
for _, spec := range decl.Specs {
valueSpec, ok := spec.(*ast.ValueSpec)
if !ok {
continue
}
for _, value := range valueSpec.Values {
funcLit, ok := value.(*ast.FuncLit)
if !ok {
continue
}
a.addStatIfNotIgnored(funcLit, valueSpec.Names[0].Name, decl.Doc)
}
}
}
}
func (a *fileAnalyzer) addStatIfNotIgnored(node ast.Node, funcName string, doc *ast.CommentGroup) {
if parseDirectives(doc).HasIgnore() {
return
}
a.stats = append(a.stats, Stat{
PkgName: a.file.Name.Name,
FuncName: funcName,
Complexity: Complexity(node),
Pos: a.fileSet.Position(node.Pos()),
})
}
// funcName returns the name representation of a function or method:
// "(Type).Name" for methods or simply "Name" for functions.
func funcName(fn *ast.FuncDecl) string {
if fn.Recv != nil {
if fn.Recv.NumFields() > 0 {
typ := fn.Recv.List[0].Type
return fmt.Sprintf("(%s).%s", recvString(typ), fn.Name)
}
}
return fn.Name.Name
}
// recvString returns a string representation of recv of the
// form "T", "*T", or "BADRECV" (if not a proper receiver type).
func recvString(recv ast.Expr) string {
switch t := recv.(type) {
case *ast.Ident:
return t.Name
case *ast.StarExpr:
return "*" + recvString(t.X)
}
return "BADRECV"
}