1
2
3
4
5
6 package directive
7
8 import (
9 "go/ast"
10 "go/parser"
11 "go/token"
12 "strings"
13 "unicode"
14 "unicode/utf8"
15
16 "golang.org/x/tools/go/analysis"
17 "golang.org/x/tools/go/analysis/passes/internal/analysisutil"
18 )
19
20 const Doc = `check Go toolchain directives such as //go:debug
21
22 This analyzer checks for problems with known Go toolchain directives
23 in all Go source files in a package directory, even those excluded by
24 //go:build constraints, and all non-Go source files too.
25
26 For //go:debug (see https://go.dev/doc/godebug), the analyzer checks
27 that the directives are placed only in Go source files, only above the
28 package comment, and only in package main or *_test.go files.
29
30 Support for other known directives may be added in the future.
31
32 This analyzer does not check //go:build, which is handled by the
33 buildtag analyzer.
34 `
35
36 var Analyzer = &analysis.Analyzer{
37 Name: "directive",
38 Doc: Doc,
39 URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/directive",
40 Run: runDirective,
41 }
42
43 func runDirective(pass *analysis.Pass) (interface{}, error) {
44 for _, f := range pass.Files {
45 checkGoFile(pass, f)
46 }
47 for _, name := range pass.OtherFiles {
48 if err := checkOtherFile(pass, name); err != nil {
49 return nil, err
50 }
51 }
52 for _, name := range pass.IgnoredFiles {
53 if strings.HasSuffix(name, ".go") {
54 f, err := parser.ParseFile(pass.Fset, name, nil, parser.ParseComments)
55 if err != nil {
56
57 continue
58 }
59 checkGoFile(pass, f)
60 } else {
61 if err := checkOtherFile(pass, name); err != nil {
62 return nil, err
63 }
64 }
65 }
66 return nil, nil
67 }
68
69 func checkGoFile(pass *analysis.Pass, f *ast.File) {
70 check := newChecker(pass, pass.Fset.File(f.Package).Name(), f)
71
72 for _, group := range f.Comments {
73
74 if group.End()+1 >= f.Package {
75 check.inHeader = false
76 }
77
78
79 if group.Pos() >= f.Package {
80 check.inHeader = false
81 }
82
83
84 for _, c := range group.List {
85 check.comment(c.Slash, c.Text)
86 }
87 }
88 }
89
90 func checkOtherFile(pass *analysis.Pass, filename string) error {
91
92
93 content, tf, err := analysisutil.ReadFile(pass.Fset, filename)
94 if err != nil {
95 return err
96 }
97
98 check := newChecker(pass, filename, nil)
99 check.nonGoFile(token.Pos(tf.Base()), string(content))
100 return nil
101 }
102
103 type checker struct {
104 pass *analysis.Pass
105 filename string
106 file *ast.File
107 inHeader bool
108 inStar bool
109 }
110
111 func newChecker(pass *analysis.Pass, filename string, file *ast.File) *checker {
112 return &checker{
113 pass: pass,
114 filename: filename,
115 file: file,
116 inHeader: true,
117 }
118 }
119
120 func (check *checker) nonGoFile(pos token.Pos, fullText string) {
121
122 text := fullText
123 inStar := false
124 for text != "" {
125 offset := len(fullText) - len(text)
126 var line string
127 line, text, _ = strings.Cut(text, "\n")
128
129 if !inStar && strings.HasPrefix(line, "//") {
130 check.comment(pos+token.Pos(offset), line)
131 continue
132 }
133
134
135
136 for {
137 line = strings.TrimSpace(line)
138 if inStar {
139 var ok bool
140 _, line, ok = strings.Cut(line, "*/")
141 if !ok {
142 break
143 }
144 inStar = false
145 continue
146 }
147 line, inStar = stringsCutPrefix(line, "/*")
148 if !inStar {
149 break
150 }
151 }
152 if line != "" {
153
154
155
156
157
158
159
160 break
161 }
162 }
163 }
164
165 func (check *checker) comment(pos token.Pos, line string) {
166 if !strings.HasPrefix(line, "//go:") {
167 return
168 }
169
170 if i := strings.Index(line, " // ERROR "); i >= 0 {
171 line = line[:i]
172 }
173
174 verb := line
175 if i := strings.IndexFunc(verb, unicode.IsSpace); i >= 0 {
176 verb = verb[:i]
177 if line[i] != ' ' && line[i] != '\t' && line[i] != '\n' {
178 r, _ := utf8.DecodeRuneInString(line[i:])
179 check.pass.Reportf(pos, "invalid space %#q in %s directive", r, verb)
180 }
181 }
182
183 switch verb {
184 default:
185
186
187
188
189 case "//go:build":
190
191
192 case "//go:debug":
193 if check.file == nil {
194 check.pass.Reportf(pos, "//go:debug directive only valid in Go source files")
195 } else if check.file.Name.Name != "main" && !strings.HasSuffix(check.filename, "_test.go") {
196 check.pass.Reportf(pos, "//go:debug directive only valid in package main or test")
197 } else if !check.inHeader {
198 check.pass.Reportf(pos, "//go:debug directive only valid before package declaration")
199 }
200 }
201 }
202
203
204 func stringsCutPrefix(s, prefix string) (after string, found bool) {
205 if !strings.HasPrefix(s, prefix) {
206 return s, false
207 }
208 return s[len(prefix):], true
209 }
210
View as plain text