-
-
Notifications
You must be signed in to change notification settings - Fork 1
/
finder.go
157 lines (134 loc) · 4.73 KB
/
finder.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
155
156
157
package golang
import (
"fmt"
"go/parser"
"go/token"
"strings"
"github.com/dave/dst"
"github.com/dave/dst/decorator"
"github.com/modernice/jotbot/internal/nodes"
"golang.org/x/exp/slices"
)
// Finder locates identifiers in Go source code, taking into account options for
// including test functions and documented entities. It analyzes the provided
// code to produce a sorted list of exported names. The search can be customized
// through options to either include or exclude test functions and documented
// identifiers. When examining interface types, it also identifies and includes
// their exported methods. Finder returns a slice of strings representing the
// found identifiers and any errors encountered during the analysis process.
type Finder struct {
findTests bool
includeDocumented bool
}
// FinderOption configures the behavior of a [*Finder] by setting its internal
// options. It is applied when constructing a new [*Finder] instance, allowing
// customization such as whether to include tests or documented entities in the
// search results.
type FinderOption func(*Finder)
// FindTests configures a Finder instance to determine whether it should
// identify test functions during code analysis. If the provided argument is
// true, the Finder will include test functions in its findings; otherwise, it
// will exclude them. This option can be passed to NewFinder to customize the
// Finder's behavior.
func FindTests(find bool) FinderOption {
return func(f *Finder) {
f.findTests = find
}
}
// IncludeDocumented configures a Finder to consider documented entities during
// the search. When set to true, entities with associated documentation will be
// included in the findings; otherwise, they will be excluded. This option is
// used when creating a new Finder instance.
func IncludeDocumented(include bool) FinderOption {
return func(f *Finder) {
f.includeDocumented = include
}
}
// NewFinder constructs a new Finder with optional configurations provided by
// FinderOptions. It returns a pointer to the initialized Finder.
func NewFinder(opts ...FinderOption) *Finder {
var f Finder
for _, opt := range opts {
opt(&f)
}
return &f
}
// Find searches through the provided code for identifiers that are eligible
// based on the Finder's configuration. It returns a sorted slice of strings
// containing these identifiers and an error if the code cannot be parsed or
// another issue occurs. Identifiers from function declarations, type
// specifications, and value specifications are included unless they are
// filtered out by the Finder's settings, such as excluding test functions or
// documented identifiers.
func (f *Finder) Find(code []byte) ([]string, error) {
var findings []string
fset := token.NewFileSet()
node, err := decorator.ParseFile(fset, "", code, parser.ParseComments|parser.SkipObjectResolution)
if err != nil {
return nil, fmt.Errorf("parse code: %w", err)
}
for _, node := range node.Decls {
switch node := node.(type) {
case *dst.FuncDecl:
if !f.findTests && isTestFunction(node) {
break
}
if !f.includeDocumented && nodes.HasDoc(node.Decs.NodeDecs.Start) {
break
}
if identifier, exported := nodes.Identifier(node); exported {
findings = append(findings, identifier)
}
case *dst.GenDecl:
if !f.includeDocumented && nodes.HasDoc(node.Decs.NodeDecs.Start) {
break
}
if len(node.Specs) == 0 {
break
}
for _, spec := range node.Specs {
switch spec := spec.(type) {
case *dst.TypeSpec:
if f.includeDocumented || !nodes.HasDoc(spec.Decs.NodeDecs.Start) {
if identifier, exported := nodes.Identifier(spec); exported {
findings = append(findings, identifier)
}
}
if isInterface(spec) {
findings = append(findings, f.findInterfaceMethods(spec)...)
}
case *dst.ValueSpec:
if f.includeDocumented || !nodes.HasDoc(spec.Decs.NodeDecs.Start) {
if identifier, exported := nodes.Identifier(spec); exported {
findings = append(findings, identifier)
}
}
}
}
}
}
slices.Sort(findings)
return findings, nil
}
func (f *Finder) findInterfaceMethods(spec *dst.TypeSpec) []string {
var findings []string
ifaceName := spec.Name.Name
for _, method := range spec.Type.(*dst.InterfaceType).Methods.List {
if len(method.Names) == 0 {
continue
}
name := method.Names[0].Name
ident := fmt.Sprintf("func:%s.%s", ifaceName, name)
if nodes.IsExportedIdentifier(ident) && (f.includeDocumented || !nodes.HasDoc(method.Decs.Start)) {
findings = append(findings, ident)
}
}
return findings
}
func isInterface(spec *dst.TypeSpec) bool {
_, ok := spec.Type.(*dst.InterfaceType)
return ok
}
func isTestFunction(node *dst.FuncDecl) bool {
return strings.HasPrefix(node.Name.Name, "Test")
}