Skip to content

Commit bcc7794

Browse files
rscgopherbot
authored andcommitted
go/analysis/passes/directive: add directive analyzer
The directive analyzer is a generalized version of the buildtag analyzer, meant to apply checks about any directives (//go:... lines) found in source code. For now it only checks the placement of //go:debug lines. For golang/go#56986. Change-Id: I2bd3d743c44554711ada90f6ee53b6195dc55bcb Reviewed-on: https://go-review.googlesource.com/c/tools/+/462216 Run-TryBot: Russ Cox <[email protected]> gopls-CI: kokoro <[email protected]> Reviewed-by: Alan Donovan <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Tim King <[email protected]> Auto-Submit: Russ Cox <[email protected]>
1 parent 33d416e commit bcc7794

File tree

14 files changed

+375
-5
lines changed

14 files changed

+375
-5
lines changed

go/analysis/passes/buildtag/buildtag.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import (
2020
"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
2121
)
2222

23-
const Doc = "check that +build tags are well-formed and correctly located"
23+
const Doc = "check //go:build and // +build directives"
2424

2525
var Analyzer = &analysis.Analyzer{
2626
Name: "buildtag",

go/analysis/passes/buildtag/buildtag_old.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import (
2222
"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
2323
)
2424

25-
const Doc = "check that +build tags are well-formed and correctly located"
25+
const Doc = "check // +build directives"
2626

2727
var Analyzer = &analysis.Analyzer{
2828
Name: "buildtag",
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// Package directive defines an Analyzer that checks known Go toolchain directives.
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+
Run: runDirective,
40+
}
41+
42+
func runDirective(pass *analysis.Pass) (interface{}, error) {
43+
for _, f := range pass.Files {
44+
checkGoFile(pass, f)
45+
}
46+
for _, name := range pass.OtherFiles {
47+
if err := checkOtherFile(pass, name); err != nil {
48+
return nil, err
49+
}
50+
}
51+
for _, name := range pass.IgnoredFiles {
52+
if strings.HasSuffix(name, ".go") {
53+
f, err := parser.ParseFile(pass.Fset, name, nil, parser.ParseComments)
54+
if err != nil {
55+
// Not valid Go source code - not our job to diagnose, so ignore.
56+
continue
57+
}
58+
checkGoFile(pass, f)
59+
} else {
60+
if err := checkOtherFile(pass, name); err != nil {
61+
return nil, err
62+
}
63+
}
64+
}
65+
return nil, nil
66+
}
67+
68+
func checkGoFile(pass *analysis.Pass, f *ast.File) {
69+
check := newChecker(pass, pass.Fset.File(f.Package).Name(), f)
70+
71+
for _, group := range f.Comments {
72+
// A +build comment is ignored after or adjoining the package declaration.
73+
if group.End()+1 >= f.Package {
74+
check.inHeader = false
75+
}
76+
// A //go:build comment is ignored after the package declaration
77+
// (but adjoining it is OK, in contrast to +build comments).
78+
if group.Pos() >= f.Package {
79+
check.inHeader = false
80+
}
81+
82+
// Check each line of a //-comment.
83+
for _, c := range group.List {
84+
check.comment(c.Slash, c.Text)
85+
}
86+
}
87+
}
88+
89+
func checkOtherFile(pass *analysis.Pass, filename string) error {
90+
// We cannot use the Go parser, since is not a Go source file.
91+
// Read the raw bytes instead.
92+
content, tf, err := analysisutil.ReadFile(pass.Fset, filename)
93+
if err != nil {
94+
return err
95+
}
96+
97+
check := newChecker(pass, filename, nil)
98+
check.nonGoFile(token.Pos(tf.Base()), string(content))
99+
return nil
100+
}
101+
102+
type checker struct {
103+
pass *analysis.Pass
104+
filename string
105+
file *ast.File // nil for non-Go file
106+
inHeader bool // in file header (before package declaration)
107+
inStar bool // currently in a /* */ comment
108+
}
109+
110+
func newChecker(pass *analysis.Pass, filename string, file *ast.File) *checker {
111+
return &checker{
112+
pass: pass,
113+
filename: filename,
114+
file: file,
115+
inHeader: true,
116+
}
117+
}
118+
119+
func (check *checker) nonGoFile(pos token.Pos, fullText string) {
120+
// Process each line.
121+
text := fullText
122+
inStar := false
123+
for text != "" {
124+
offset := len(fullText) - len(text)
125+
var line string
126+
line, text, _ = stringsCut(text, "\n")
127+
128+
if !inStar && strings.HasPrefix(line, "//") {
129+
check.comment(pos+token.Pos(offset), line)
130+
continue
131+
}
132+
133+
// Skip over, cut out any /* */ comments,
134+
// to avoid being confused by a commented-out // comment.
135+
for {
136+
line = strings.TrimSpace(line)
137+
if inStar {
138+
var ok bool
139+
_, line, ok = stringsCut(line, "*/")
140+
if !ok {
141+
break
142+
}
143+
inStar = false
144+
continue
145+
}
146+
line, inStar = stringsCutPrefix(line, "/*")
147+
if !inStar {
148+
break
149+
}
150+
}
151+
if line != "" {
152+
// Found non-comment non-blank line.
153+
// Ends space for valid //go:build comments,
154+
// but also ends the fraction of the file we can
155+
// reliably parse. From this point on we might
156+
// incorrectly flag "comments" inside multiline
157+
// string constants or anything else (this might
158+
// not even be a Go program). So stop.
159+
break
160+
}
161+
}
162+
}
163+
164+
func (check *checker) comment(pos token.Pos, line string) {
165+
if !strings.HasPrefix(line, "//go:") {
166+
return
167+
}
168+
// testing hack: stop at // ERROR
169+
if i := strings.Index(line, " // ERROR "); i >= 0 {
170+
line = line[:i]
171+
}
172+
173+
verb := line
174+
if i := strings.IndexFunc(verb, unicode.IsSpace); i >= 0 {
175+
verb = verb[:i]
176+
if line[i] != ' ' && line[i] != '\t' && line[i] != '\n' {
177+
r, _ := utf8.DecodeRuneInString(line[i:])
178+
check.pass.Reportf(pos, "invalid space %#q in %s directive", r, verb)
179+
}
180+
}
181+
182+
switch verb {
183+
default:
184+
// TODO: Use the go language version for the file.
185+
// If that version is not newer than us, then we can
186+
// report unknown directives.
187+
188+
case "//go:build":
189+
// Ignore. The buildtag analyzer reports misplaced comments.
190+
191+
case "//go:debug":
192+
if check.file == nil {
193+
check.pass.Reportf(pos, "//go:debug directive only valid in Go source files")
194+
} else if check.file.Name.Name != "main" && !strings.HasSuffix(check.filename, "_test.go") {
195+
check.pass.Reportf(pos, "//go:debug directive only valid in package main or test")
196+
} else if !check.inHeader {
197+
check.pass.Reportf(pos, "//go:debug directive only valid before package declaration")
198+
}
199+
}
200+
}
201+
202+
// Go 1.18 strings.Cut.
203+
func stringsCut(s, sep string) (before, after string, found bool) {
204+
if i := strings.Index(s, sep); i >= 0 {
205+
return s[:i], s[i+len(sep):], true
206+
}
207+
return s, "", false
208+
}
209+
210+
// Go 1.20 strings.CutPrefix.
211+
func stringsCutPrefix(s, prefix string) (after string, found bool) {
212+
if !strings.HasPrefix(s, prefix) {
213+
return s, false
214+
}
215+
return s[len(prefix):], true
216+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package directive_test
6+
7+
import (
8+
"runtime"
9+
"strings"
10+
"testing"
11+
12+
"golang.org/x/tools/go/analysis"
13+
"golang.org/x/tools/go/analysis/analysistest"
14+
"golang.org/x/tools/go/analysis/passes/directive"
15+
)
16+
17+
func Test(t *testing.T) {
18+
if strings.HasPrefix(runtime.Version(), "go1.") && runtime.Version() < "go1.16" {
19+
t.Skipf("skipping on %v", runtime.Version())
20+
}
21+
analyzer := *directive.Analyzer
22+
analyzer.Run = func(pass *analysis.Pass) (interface{}, error) {
23+
defer func() {
24+
// The directive pass is unusual in that it checks the IgnoredFiles.
25+
// After analysis, add IgnoredFiles to OtherFiles so that
26+
// the test harness checks for expected diagnostics in those.
27+
// (The test harness shouldn't do this by default because most
28+
// passes can't do anything with the IgnoredFiles without type
29+
// information, which is unavailable because they are ignored.)
30+
var files []string
31+
files = append(files, pass.OtherFiles...)
32+
files = append(files, pass.IgnoredFiles...)
33+
pass.OtherFiles = files
34+
}()
35+
36+
return directive.Analyzer.Run(pass)
37+
}
38+
analysistest.Run(t, analysistest.TestData(), &analyzer, "a")
39+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build ignore
6+
7+
// want +1 `invalid space '\\u00a0' in //go:debug directive`
8+
//go:debug 00a0
9+
10+
package main
11+
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build ignore
6+
7+
package main
8+
9+
// want +1 `//go:debug directive only valid before package declaration`
10+
//go:debug panicnil=1
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// want +1 `//go:debug directive only valid in Go source files`
6+
//go:debug panicnil=1
7+
8+
/*
9+
can skip over comments
10+
//go:debug doesn't matter here
11+
*/
12+
13+
// want +1 `//go:debug directive only valid in Go source files`
14+
//go:debug panicnil=1
15+
16+
package a
17+
18+
// no error here because we can't parse this far
19+
//go:debug panicnil=1
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:debug panicnil=1
6+
7+
package p_test
8+
9+
// want +1 `//go:debug directive only valid before package declaration`
10+
//go:debug panicnil=1
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// want +1 `//go:debug directive only valid in package main or test`
6+
//go:debug panicnil=1
7+
8+
package p
9+
10+
// want +1 `//go:debug directive only valid in package main or test`
11+
//go:debug panicnil=1

gopls/doc/analyzers.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ check for common mistakes involving boolean operators
4848

4949
## **buildtag**
5050

51-
check that +build tags are well-formed and correctly located
51+
check //go:build and // +build directives
5252

5353
**Enabled by default.**
5454

@@ -106,6 +106,26 @@ The deepequalerrors checker looks for calls of the form:
106106
where err1 and err2 are errors. Using reflect.DeepEqual to compare
107107
errors is discouraged.
108108

109+
**Enabled by default.**
110+
111+
## **directive**
112+
113+
check Go toolchain directives such as //go:debug
114+
115+
This analyzer checks for problems with known Go toolchain directives
116+
in all Go source files in a package directory, even those excluded by
117+
//go:build constraints, and all non-Go source files too.
118+
119+
For //go:debug (see https://go.dev/doc/godebug), the analyzer checks
120+
that the directives are placed only in Go source files, only above the
121+
package comment, and only in package main or *_test.go files.
122+
123+
Support for other known directives may be added in the future.
124+
125+
This analyzer does not check //go:build, which is handled by the
126+
buildtag analyzer.
127+
128+
109129
**Enabled by default.**
110130

111131
## **embed**

0 commit comments

Comments
 (0)