-
Notifications
You must be signed in to change notification settings - Fork 7
/
getters.go
272 lines (243 loc) · 6.83 KB
/
getters.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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
package godot
import (
"errors"
"fmt"
"go/ast"
"go/token"
"os"
"path/filepath"
"regexp"
"strings"
)
var (
errEmptyInput = errors.New("empty input")
errUnsuitableInput = errors.New("unsuitable input")
)
// specialReplacer is a replacer for some types of special lines in comments,
// which shouldn't be checked. For example, if comment ends with a block of
// code it should not necessarily have a period at the end.
const specialReplacer = "<godotSpecialReplacer>"
type parsedFile struct {
fset *token.FileSet
file *ast.File
lines []string
}
func newParsedFile(file *ast.File, fset *token.FileSet) (*parsedFile, error) {
if file == nil || fset == nil || len(file.Comments) == 0 {
return nil, errEmptyInput
}
pf := parsedFile{
fset: fset,
file: file,
}
// Read original file. This is necessary for making a replacements for
// inline comments. I couldn't find a better way to get original line
// with code and comment without reading the file. Function `Format`
// from "go/format" won't help here if the original file is not gofmt-ed.
filename := getFilename(fset, file)
if !strings.HasSuffix(filename, ".go") {
return nil, errEmptyInput
}
var err error
pf.lines, err = readFile(filename)
if err != nil {
return nil, fmt.Errorf("read file: %w", err)
}
return &pf, nil
}
// getComments extracts comments from a file.
func (pf *parsedFile) getComments(scope Scope, exclude []*regexp.Regexp) []comment {
var comments []comment
decl := pf.getDeclarationComments(exclude)
switch scope {
case AllScope:
// All comments
comments = pf.getAllComments(exclude)
case TopLevelScope:
// All top level comments and comments from the inside
// of top level blocks
comments = append(
pf.getBlockComments(exclude),
pf.getTopLevelComments(exclude)...,
)
case DeclScope:
// Top level declaration comments and comments from the inside
// of top level blocks
comments = append(pf.getBlockComments(exclude), decl...)
}
// Set `decl` flag
setDecl(comments, decl)
return comments
}
// getBlockComments gets comments from the inside of top level blocks:
// var (...), const (...).
func (pf *parsedFile) getBlockComments(exclude []*regexp.Regexp) []comment {
var comments []comment
for _, decl := range pf.file.Decls {
d, ok := decl.(*ast.GenDecl)
if !ok {
continue
}
// No parenthesis == no block
if d.Lparen == 0 {
continue
}
for _, c := range pf.file.Comments {
if c == nil || len(c.List) == 0 {
continue
}
// Skip comments outside this block
if d.Lparen > c.Pos() || c.Pos() > d.Rparen {
continue
}
// Skip comments that are not top-level for this block
// (the block itself is top level, so comments inside this block
// would be on column 2)
//nolint:gomnd
if pf.fset.Position(c.Pos()).Column != 2 {
continue
}
firstLine := pf.fset.Position(c.Pos()).Line
lastLine := pf.fset.Position(c.End()).Line
comments = append(comments, comment{
lines: pf.lines[firstLine-1 : lastLine],
text: getText(c, exclude),
start: pf.fset.Position(c.List[0].Slash),
})
}
}
return comments
}
// getTopLevelComments gets all top level comments.
func (pf *parsedFile) getTopLevelComments(exclude []*regexp.Regexp) []comment {
var comments []comment //nolint:prealloc
for _, c := range pf.file.Comments {
if c == nil || len(c.List) == 0 {
continue
}
if pf.fset.Position(c.Pos()).Column != 1 {
continue
}
firstLine := pf.fset.Position(c.Pos()).Line
lastLine := pf.fset.Position(c.End()).Line
comments = append(comments, comment{
lines: pf.lines[firstLine-1 : lastLine],
text: getText(c, exclude),
start: pf.fset.Position(c.List[0].Slash),
})
}
return comments
}
// getDeclarationComments gets top level declaration comments.
func (pf *parsedFile) getDeclarationComments(exclude []*regexp.Regexp) []comment {
var comments []comment //nolint:prealloc
for _, decl := range pf.file.Decls {
var cg *ast.CommentGroup
switch d := decl.(type) {
case *ast.GenDecl:
cg = d.Doc
case *ast.FuncDecl:
cg = d.Doc
}
if cg == nil || len(cg.List) == 0 {
continue
}
firstLine := pf.fset.Position(cg.Pos()).Line
lastLine := pf.fset.Position(cg.End()).Line
comments = append(comments, comment{
lines: pf.lines[firstLine-1 : lastLine],
text: getText(cg, exclude),
start: pf.fset.Position(cg.List[0].Slash),
})
}
return comments
}
// getAllComments gets every single comment from the file.
func (pf *parsedFile) getAllComments(exclude []*regexp.Regexp) []comment {
var comments []comment //nolint:prealloc
for _, c := range pf.file.Comments {
if c == nil || len(c.List) == 0 {
continue
}
firstLine := pf.fset.Position(c.Pos()).Line
lastLine := pf.fset.Position(c.End()).Line
comments = append(comments, comment{
lines: pf.lines[firstLine-1 : lastLine],
start: pf.fset.Position(c.List[0].Slash),
text: getText(c, exclude),
})
}
return comments
}
// getText extracts text from comment. If the comment is a special block
// (e.g., CGO code), a block of empty lines is returned. If comment contains
// special lines (e.g., tags or indented code examples), they are replaced
// with `specialReplacer` to skip checks for them.
// The result can be multiline.
func getText(comment *ast.CommentGroup, exclude []*regexp.Regexp) (s string) {
if len(comment.List) > 0 && isSpecialBlock(comment.List[0].Text) {
return ""
}
for _, c := range comment.List {
text := c.Text
isBlock := false
if strings.HasPrefix(c.Text, "/*") {
isBlock = true
text = strings.TrimPrefix(text, "/*")
text = strings.TrimSuffix(text, "*/")
}
for _, line := range strings.Split(text, "\n") {
if isSpecialLine(line) {
s += specialReplacer + "\n"
continue
}
if !isBlock {
line = strings.TrimPrefix(line, "//")
}
if matchAny(line, exclude) {
s += specialReplacer + "\n"
continue
}
s += line + "\n"
}
}
if len(s) == 0 {
return ""
}
return s[:len(s)-1] // trim last "\n"
}
// readFile reads file and returns its lines as strings.
func readFile(filename string) ([]string, error) {
f, err := os.ReadFile(filepath.Clean(filename))
if err != nil {
return nil, err //nolint:wrapcheck
}
return strings.Split(string(f), "\n"), nil
}
// setDecl sets `decl` flag to comments which are declaration comments.
func setDecl(comments, decl []comment) {
for _, d := range decl {
for i, c := range comments {
if d.start == c.start {
comments[i].decl = true
break
}
}
}
}
// matchAny checks if string matches any of given regexps.
func matchAny(s string, rr []*regexp.Regexp) bool {
for _, re := range rr {
if re.MatchString(s) {
return true
}
}
return false
}
func getFilename(fset *token.FileSet, file *ast.File) string {
filename := fset.PositionFor(file.Pos(), true).Filename
if !strings.HasSuffix(filename, ".go") {
return fset.PositionFor(file.Pos(), false).Filename
}
return filename
}