-
Notifications
You must be signed in to change notification settings - Fork 83
/
analyze.go
154 lines (139 loc) · 3.82 KB
/
analyze.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
// 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"
"io/fs"
"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.WalkDir(dirname, func(path string, entry fs.DirEntry, err error) error {
if isSkipDir(entry) {
return filepath.SkipDir
}
if err == nil && isGoFile(entry) {
stats = analyzeFile(path, ignore, stats)
}
return err
})
return stats
}
var skipDirs = map[string]bool{
"testdata": true,
"vendor": true,
}
func isSkipDir(entry fs.DirEntry) bool {
return entry.IsDir() && (skipDirs[entry.Name()] ||
(strings.HasPrefix(entry.Name(), ".") && entry.Name() != "." && entry.Name() != "..") ||
strings.HasPrefix(entry.Name(), "_"))
}
func isGoFile(entry fs.DirEntry) bool {
return !entry.IsDir() && strings.HasSuffix(entry.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
}