-
Notifications
You must be signed in to change notification settings - Fork 4
/
godot.go
151 lines (132 loc) · 3.56 KB
/
godot.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
// Package godot checks if all top-level comments contain a period at the
// end of the last sentence if needed.
package godot
import (
"go/ast"
"go/token"
"regexp"
"strings"
)
const noPeriodMessage = "Top level comment should end in a period"
// Message contains a message of linting error.
type Message struct {
Pos token.Position
Message string
}
// Settings contains linter settings.
type Settings struct {
// Check all top-level comments, not only declarations
CheckAll bool
}
var (
// List of valid last characters.
lastChars = []string{".", "?", "!"}
// Special tags in comments like "nolint" or "build".
tags = regexp.MustCompile("^[a-z]+:")
// Special hashtags in comments like "#nosec".
hashtags = regexp.MustCompile("^#[a-z]+ ")
// URL at the end of the line.
endURL = regexp.MustCompile(`[a-z]+://[^\s]+$`)
)
// Run runs this linter on the provided code.
func Run(file *ast.File, fset *token.FileSet, settings Settings) []Message {
msgs := []Message{}
// Check all top-level comments
if settings.CheckAll {
for _, group := range file.Comments {
if ok, msg := check(fset, group); !ok {
msgs = append(msgs, msg)
}
}
return msgs
}
// Check only declaration comments
for _, decl := range file.Decls {
switch d := decl.(type) {
case *ast.GenDecl:
if ok, msg := check(fset, d.Doc); !ok {
msgs = append(msgs, msg)
}
case *ast.FuncDecl:
if ok, msg := check(fset, d.Doc); !ok {
msgs = append(msgs, msg)
}
}
}
return msgs
}
func check(fset *token.FileSet, group *ast.CommentGroup) (ok bool, msg Message) {
if group == nil || len(group.List) == 0 {
return true, Message{}
}
// Check only top-level comments
if fset.Position(group.Pos()).Column > 1 {
return true, Message{}
}
// Get last element from comment group - it can be either
// last (or single) line for "//"-comment, or multiline string
// for "/*"-comment
last := group.List[len(group.List)-1]
line, ok := checkComment(last.Text)
if ok {
return true, Message{}
}
pos := fset.Position(last.Slash)
pos.Line += line
return false, Message{
Pos: pos,
Message: noPeriodMessage,
}
}
func checkComment(comment string) (line int, ok bool) {
// Check last line of "//"-comment
if strings.HasPrefix(comment, "//") {
comment = strings.TrimPrefix(comment, "//")
return 0, checkLastChar(comment)
}
// Skip cgo code blocks
// TODO: Find a better way to detect cgo code.
if strings.Contains(comment, "#include") || strings.Contains(comment, "#define") {
return 0, true
}
// Check last non-empty line in multiline "/*"-comment block
lines := strings.Split(comment, "\n")
var i int
for i = len(lines) - 1; i >= 0; i-- {
if s := strings.TrimSpace(lines[i]); s == "*/" || s == "" {
continue
}
break
}
comment = strings.TrimPrefix(lines[i], "/*")
comment = strings.TrimSuffix(comment, "*/")
return i, checkLastChar(comment)
}
func checkLastChar(s string) bool {
// Don't check comments starting with space indentation - they may
// contain code examples, which shouldn't end with period
if strings.HasPrefix(s, " ") || strings.HasPrefix(s, " \t") || strings.HasPrefix(s, "\t") {
return true
}
// Skip cgo export tags: https://golang.org/cmd/cgo/#hdr-C_references_to_Go
if strings.HasPrefix(s, "export") {
return true
}
s = strings.TrimSpace(s)
if tags.MatchString(s) ||
hashtags.MatchString(s) ||
endURL.MatchString(s) ||
strings.HasPrefix(s, "+build") {
return true
}
// Don't check empty lines
if s == "" {
return true
}
for _, ch := range lastChars {
if string(s[len(s)-1]) == ch {
return true
}
}
return false
}