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
75 if group.Pos() >= f.Package {
76 check.inHeader = false
77 }
78
79
80 for _, c := range group.List {
81 check.comment(c.Slash, c.Text)
82 }
83 }
84 }
85
86 func checkOtherFile(pass *analysis.Pass, filename string) error {
87
88
89 content, tf, err := analysisutil.ReadFile(pass, filename)
90 if err != nil {
91 return err
92 }
93
94 check := newChecker(pass, filename, nil)
95 check.nonGoFile(token.Pos(tf.Base()), string(content))
96 return nil
97 }
98
99 type checker struct {
100 pass *analysis.Pass
101 filename string
102 file *ast.File
103 inHeader bool
104 }
105
106 func newChecker(pass *analysis.Pass, filename string, file *ast.File) *checker {
107 return &checker{
108 pass: pass,
109 filename: filename,
110 file: file,
111 inHeader: true,
112 }
113 }
114
115 func (check *checker) nonGoFile(pos token.Pos, fullText string) {
116
117 text := fullText
118 inStar := false
119 for text != "" {
120 offset := len(fullText) - len(text)
121 var line string
122 line, text, _ = strings.Cut(text, "\n")
123
124 if !inStar && strings.HasPrefix(line, "//") {
125 check.comment(pos+token.Pos(offset), line)
126 continue
127 }
128
129
130
131 for {
132 line = strings.TrimSpace(line)
133 if inStar {
134 var ok bool
135 _, line, ok = strings.Cut(line, "*/")
136 if !ok {
137 break
138 }
139 inStar = false
140 continue
141 }
142 line, inStar = stringsCutPrefix(line, "/*")
143 if !inStar {
144 break
145 }
146 }
147 if line != "" {
148
149
150
151
152
153
154
155 break
156 }
157 }
158 }
159
160 func (check *checker) comment(pos token.Pos, line string) {
161 if !strings.HasPrefix(line, "//go:") {
162 return
163 }
164
165 if i := strings.Index(line, " // ERROR "); i >= 0 {
166 line = line[:i]
167 }
168
169 verb := line
170 if i := strings.IndexFunc(verb, unicode.IsSpace); i >= 0 {
171 verb = verb[:i]
172 if line[i] != ' ' && line[i] != '\t' && line[i] != '\n' {
173 r, _ := utf8.DecodeRuneInString(line[i:])
174 check.pass.Reportf(pos, "invalid space %#q in %s directive", r, verb)
175 }
176 }
177
178 switch verb {
179 default:
180
181
182
183
184 case "//go:build":
185
186
187 case "//go:debug":
188 if check.file == nil {
189 check.pass.Reportf(pos, "//go:debug directive only valid in Go source files")
190 } else if check.file.Name.Name != "main" && !strings.HasSuffix(check.filename, "_test.go") {
191 check.pass.Reportf(pos, "//go:debug directive only valid in package main or test")
192 } else if !check.inHeader {
193 check.pass.Reportf(pos, "//go:debug directive only valid before package declaration")
194 }
195 }
196 }
197
198
199 func stringsCutPrefix(s, prefix string) (after string, found bool) {
200 if !strings.HasPrefix(s, prefix) {
201 return s, false
202 }
203 return s[len(prefix):], true
204 }
205
View as plain text