1package godot
2
3import (
4	"errors"
5	"fmt"
6	"go/ast"
7	"go/token"
8	"io/ioutil"
9	"regexp"
10	"strings"
11)
12
13var errEmptyInput = errors.New("empty input")
14
15// specialReplacer is a replacer for some types of special lines in comments,
16// which shouldn't be checked. For example, if comment ends with a block of
17// code it should not necessarily have a period at the end.
18const specialReplacer = "<godotSpecialReplacer>"
19
20type parsedFile struct {
21	fset  *token.FileSet
22	file  *ast.File
23	lines []string
24}
25
26func newParsedFile(file *ast.File, fset *token.FileSet) (*parsedFile, error) {
27	if file == nil || fset == nil || len(file.Comments) == 0 {
28		return nil, errEmptyInput
29	}
30
31	pf := parsedFile{
32		fset: fset,
33		file: file,
34	}
35
36	var err error
37
38	// Read original file. This is necessary for making a replacements for
39	// inline comments. I couldn't find a better way to get original line
40	// with code and comment without reading the file. Function `Format`
41	// from "go/format" won't help here if the original file is not gofmt-ed.
42	pf.lines, err = readFile(file, fset)
43	if err != nil {
44		return nil, fmt.Errorf("read file: %v", err)
45	}
46
47	// Check consistency to avoid checking slice indexes in each function
48	lastComment := pf.file.Comments[len(pf.file.Comments)-1]
49	if p := pf.fset.Position(lastComment.End()); len(pf.lines) < p.Line {
50		return nil, fmt.Errorf("inconsistence between file and AST: %s", p.Filename)
51	}
52
53	return &pf, nil
54}
55
56// getComments extracts comments from a file.
57func (pf *parsedFile) getComments(scope Scope, exclude []*regexp.Regexp) []comment {
58	var comments []comment
59	decl := pf.getDeclarationComments(exclude)
60	switch scope {
61	case AllScope:
62		// All comments
63		comments = pf.getAllComments(exclude)
64	case TopLevelScope:
65		// All top level comments and comments from the inside
66		// of top level blocks
67		comments = append(
68			pf.getBlockComments(exclude),
69			pf.getTopLevelComments(exclude)...,
70		)
71	default:
72		// Top level declaration comments and comments from the inside
73		// of top level blocks
74		comments = append(pf.getBlockComments(exclude), decl...)
75	}
76
77	// Set `decl` flag
78	setDecl(comments, decl)
79
80	return comments
81}
82
83// getBlockComments gets comments from the inside of top level blocks:
84// var (...), const (...).
85func (pf *parsedFile) getBlockComments(exclude []*regexp.Regexp) []comment {
86	var comments []comment
87	for _, decl := range pf.file.Decls {
88		d, ok := decl.(*ast.GenDecl)
89		if !ok {
90			continue
91		}
92		// No parenthesis == no block
93		if d.Lparen == 0 {
94			continue
95		}
96		for _, c := range pf.file.Comments {
97			if c == nil || len(c.List) == 0 {
98				continue
99			}
100			// Skip comments outside this block
101			if d.Lparen > c.Pos() || c.Pos() > d.Rparen {
102				continue
103			}
104			// Skip comments that are not top-level for this block
105			// (the block itself is top level, so comments inside this block
106			// would be on column 2)
107			// nolint: gomnd
108			if pf.fset.Position(c.Pos()).Column != 2 {
109				continue
110			}
111			firstLine := pf.fset.Position(c.Pos()).Line
112			lastLine := pf.fset.Position(c.End()).Line
113			comments = append(comments, comment{
114				lines: pf.lines[firstLine-1 : lastLine],
115				text:  getText(c, exclude),
116				start: pf.fset.Position(c.List[0].Slash),
117			})
118		}
119	}
120	return comments
121}
122
123// getTopLevelComments gets all top level comments.
124func (pf *parsedFile) getTopLevelComments(exclude []*regexp.Regexp) []comment {
125	var comments []comment // nolint: prealloc
126	for _, c := range pf.file.Comments {
127		if c == nil || len(c.List) == 0 {
128			continue
129		}
130		if pf.fset.Position(c.Pos()).Column != 1 {
131			continue
132		}
133		firstLine := pf.fset.Position(c.Pos()).Line
134		lastLine := pf.fset.Position(c.End()).Line
135		comments = append(comments, comment{
136			lines: pf.lines[firstLine-1 : lastLine],
137			text:  getText(c, exclude),
138			start: pf.fset.Position(c.List[0].Slash),
139		})
140	}
141	return comments
142}
143
144// getDeclarationComments gets top level declaration comments.
145func (pf *parsedFile) getDeclarationComments(exclude []*regexp.Regexp) []comment {
146	var comments []comment // nolint: prealloc
147	for _, decl := range pf.file.Decls {
148		var cg *ast.CommentGroup
149		switch d := decl.(type) {
150		case *ast.GenDecl:
151			cg = d.Doc
152		case *ast.FuncDecl:
153			cg = d.Doc
154		}
155
156		if cg == nil || len(cg.List) == 0 {
157			continue
158		}
159
160		firstLine := pf.fset.Position(cg.Pos()).Line
161		lastLine := pf.fset.Position(cg.End()).Line
162		comments = append(comments, comment{
163			lines: pf.lines[firstLine-1 : lastLine],
164			text:  getText(cg, exclude),
165			start: pf.fset.Position(cg.List[0].Slash),
166		})
167	}
168	return comments
169}
170
171// getAllComments gets every single comment from the file.
172func (pf *parsedFile) getAllComments(exclude []*regexp.Regexp) []comment {
173	var comments []comment //nolint: prealloc
174	for _, c := range pf.file.Comments {
175		if c == nil || len(c.List) == 0 {
176			continue
177		}
178		firstLine := pf.fset.Position(c.Pos()).Line
179		lastLine := pf.fset.Position(c.End()).Line
180		comments = append(comments, comment{
181			lines: pf.lines[firstLine-1 : lastLine],
182			start: pf.fset.Position(c.List[0].Slash),
183			text:  getText(c, exclude),
184		})
185	}
186	return comments
187}
188
189// getText extracts text from comment. If comment is a special block
190// (e.g., CGO code), a block of empty lines is returned. If comment contains
191// special lines (e.g., tags or indented code examples), they are replaced
192// with `specialReplacer` to skip checks for it.
193// The result can be multiline.
194func getText(comment *ast.CommentGroup, exclude []*regexp.Regexp) (s string) {
195	if len(comment.List) == 1 &&
196		strings.HasPrefix(comment.List[0].Text, "/*") &&
197		isSpecialBlock(comment.List[0].Text) {
198		return ""
199	}
200
201	for _, c := range comment.List {
202		text := c.Text
203		isBlock := false
204		if strings.HasPrefix(c.Text, "/*") {
205			isBlock = true
206			text = strings.TrimPrefix(text, "/*")
207			text = strings.TrimSuffix(text, "*/")
208		}
209		for _, line := range strings.Split(text, "\n") {
210			if isSpecialLine(line) {
211				s += specialReplacer + "\n"
212				continue
213			}
214			if !isBlock {
215				line = strings.TrimPrefix(line, "//")
216			}
217			if matchAny(line, exclude) {
218				s += specialReplacer + "\n"
219				continue
220			}
221			s += line + "\n"
222		}
223	}
224	if len(s) == 0 {
225		return ""
226	}
227	return s[:len(s)-1] // trim last "\n"
228}
229
230// readFile reads file and returns it's lines as strings.
231func readFile(file *ast.File, fset *token.FileSet) ([]string, error) {
232	fname := fset.File(file.Package)
233	f, err := ioutil.ReadFile(fname.Name())
234	if err != nil {
235		return nil, err
236	}
237	return strings.Split(string(f), "\n"), nil
238}
239
240// setDecl sets `decl` flag to comments which are declaration comments.
241func setDecl(comments, decl []comment) {
242	for _, d := range decl {
243		for i, c := range comments {
244			if d.start == c.start {
245				comments[i].decl = true
246				break
247			}
248		}
249	}
250}
251
252// matchAny checks if string matches any of given regexps.
253func matchAny(s string, rr []*regexp.Regexp) bool {
254	for _, re := range rr {
255		if re.MatchString(s) {
256			return true
257		}
258	}
259	return false
260}
261