Skip to content

Commit b182fde

Browse files
ameowliaJay Conrod
authored andcommitted
go/analysis/passes/tests: check example output
Add check to make sure that the output comment block in a testable example is the last comment block. If the output comment block is not the last comment block then the output will not be tested and the test will always pass. Fixes: golang/go#48362 Change-Id: Iae93423d49ffc35019a1bc71e2c8d4a398301cd1 Reviewed-on: https://go-review.googlesource.com/c/tools/+/351553 Reviewed-by: Zvonimir Pavlinovic <[email protected]> Reviewed-by: Jay Conrod <[email protected]> Run-TryBot: Zvonimir Pavlinovic <[email protected]> Run-TryBot: Jay Conrod <[email protected]> gopls-CI: kokoro <[email protected]> Trust: Jay Conrod <[email protected]> Trust: Heschi Kreinick <[email protected]>
1 parent ba6b94c commit b182fde

File tree

2 files changed

+97
-3
lines changed

2 files changed

+97
-3
lines changed

go/analysis/passes/tests/testdata/src/a/a_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,46 @@ func ExampleFoo() {} // OK because a.Foo exists
5353

5454
func ExampleBar() {} // want "ExampleBar refers to unknown identifier: Bar"
5555

56+
func Example_withOutput() {
57+
// Output:
58+
// meow
59+
} // OK because output is the last comment block
60+
61+
func Example_withBadOutput() {
62+
// Output: // want "output comment block must be the last comment block"
63+
// meow
64+
65+
// todo: change to bark
66+
}
67+
68+
func Example_withBadUnorderedOutput() {
69+
// Unordered Output: // want "output comment block must be the last comment block"
70+
// meow
71+
72+
// todo: change to bark
73+
}
74+
75+
func Example_withCommentAfterFunc() {
76+
// Output: // OK because it is the last comment block
77+
// meow
78+
} // todo: change to bark
79+
80+
func Example_withOutputCommentAfterFunc() {
81+
// Output:
82+
// meow
83+
} // Output: bark // OK because output is not inside of an example
84+
85+
func Example_withMultipleOutputs() {
86+
// Output: // want "there can only be one output comment block per example"
87+
// meow
88+
89+
// Output: // want "there can only be one output comment block per example"
90+
// bark
91+
92+
// Output: // OK because it is the last output comment block
93+
// ribbit
94+
}
95+
5696
func nonTest() {} // OK because it doesn't start with "Test".
5797

5898
func (Buf) TesthasReceiver() {} // OK because it has a receiver.

go/analysis/passes/tests/tests.go

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ package tests
88

99
import (
1010
"go/ast"
11+
"go/token"
1112
"go/types"
13+
"regexp"
1214
"strings"
1315
"unicode"
1416
"unicode/utf8"
@@ -42,10 +44,10 @@ func run(pass *analysis.Pass) (interface{}, error) {
4244
// Ignore non-functions or functions with receivers.
4345
continue
4446
}
45-
4647
switch {
4748
case strings.HasPrefix(fn.Name.Name, "Example"):
48-
checkExample(pass, fn)
49+
checkExampleName(pass, fn)
50+
checkExampleOutput(pass, fn, f.Comments)
4951
case strings.HasPrefix(fn.Name.Name, "Test"):
5052
checkTest(pass, fn, "Test")
5153
case strings.HasPrefix(fn.Name.Name, "Benchmark"):
@@ -108,7 +110,59 @@ func lookup(pkg *types.Package, name string) []types.Object {
108110
return ret
109111
}
110112

111-
func checkExample(pass *analysis.Pass, fn *ast.FuncDecl) {
113+
// This pattern is taken from /go/src/go/doc/example.go
114+
var outputRe = regexp.MustCompile(`(?i)^[[:space:]]*(unordered )?output:`)
115+
116+
type commentMetadata struct {
117+
isOutput bool
118+
pos token.Pos
119+
}
120+
121+
func checkExampleOutput(pass *analysis.Pass, fn *ast.FuncDecl, fileComments []*ast.CommentGroup) {
122+
commentsInExample := []commentMetadata{}
123+
numOutputs := 0
124+
125+
// Find the comment blocks that are in the example. These comments are
126+
// guaranteed to be in order of appearance.
127+
for _, cg := range fileComments {
128+
if cg.Pos() < fn.Pos() {
129+
continue
130+
} else if cg.End() > fn.End() {
131+
break
132+
}
133+
134+
isOutput := outputRe.MatchString(cg.Text())
135+
if isOutput {
136+
numOutputs++
137+
}
138+
139+
commentsInExample = append(commentsInExample, commentMetadata{
140+
isOutput: isOutput,
141+
pos: cg.Pos(),
142+
})
143+
}
144+
145+
// Change message based on whether there are multiple output comment blocks.
146+
msg := "output comment block must be the last comment block"
147+
if numOutputs > 1 {
148+
msg = "there can only be one output comment block per example"
149+
}
150+
151+
for i, cg := range commentsInExample {
152+
// Check for output comments that are not the last comment in the example.
153+
isLast := (i == len(commentsInExample)-1)
154+
if cg.isOutput && !isLast {
155+
pass.Report(
156+
analysis.Diagnostic{
157+
Pos: cg.pos,
158+
Message: msg,
159+
},
160+
)
161+
}
162+
}
163+
}
164+
165+
func checkExampleName(pass *analysis.Pass, fn *ast.FuncDecl) {
112166
fnName := fn.Name.Name
113167
if params := fn.Type.Params; len(params.List) != 0 {
114168
pass.Reportf(fn.Pos(), "%s should be niladic", fnName)

0 commit comments

Comments
 (0)