Skip to content

Commit

Permalink
go/analysis/passes/tests: check example output
Browse files Browse the repository at this point in the history
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 <zpavlinovic@google.com>
Reviewed-by: Jay Conrod <jayconrod@google.com>
Run-TryBot: Zvonimir Pavlinovic <zpavlinovic@google.com>
Run-TryBot: Jay Conrod <jayconrod@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
Trust: Jay Conrod <jayconrod@google.com>
Trust: Heschi Kreinick <heschi@google.com>
  • Loading branch information
ameowlia authored and Jay Conrod committed Sep 28, 2021
1 parent ba6b94c commit b182fde
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 3 deletions.
40 changes: 40 additions & 0 deletions go/analysis/passes/tests/testdata/src/a/a_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,46 @@ func ExampleFoo() {} // OK because a.Foo exists

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

func Example_withOutput() {
// Output:
// meow
} // OK because output is the last comment block

func Example_withBadOutput() {
// Output: // want "output comment block must be the last comment block"
// meow

// todo: change to bark
}

func Example_withBadUnorderedOutput() {
// Unordered Output: // want "output comment block must be the last comment block"
// meow

// todo: change to bark
}

func Example_withCommentAfterFunc() {
// Output: // OK because it is the last comment block
// meow
} // todo: change to bark

func Example_withOutputCommentAfterFunc() {
// Output:
// meow
} // Output: bark // OK because output is not inside of an example

func Example_withMultipleOutputs() {
// Output: // want "there can only be one output comment block per example"
// meow

// Output: // want "there can only be one output comment block per example"
// bark

// Output: // OK because it is the last output comment block
// ribbit
}

func nonTest() {} // OK because it doesn't start with "Test".

func (Buf) TesthasReceiver() {} // OK because it has a receiver.
Expand Down
60 changes: 57 additions & 3 deletions go/analysis/passes/tests/tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ package tests

import (
"go/ast"
"go/token"
"go/types"
"regexp"
"strings"
"unicode"
"unicode/utf8"
Expand Down Expand Up @@ -42,10 +44,10 @@ func run(pass *analysis.Pass) (interface{}, error) {
// Ignore non-functions or functions with receivers.
continue
}

switch {
case strings.HasPrefix(fn.Name.Name, "Example"):
checkExample(pass, fn)
checkExampleName(pass, fn)
checkExampleOutput(pass, fn, f.Comments)
case strings.HasPrefix(fn.Name.Name, "Test"):
checkTest(pass, fn, "Test")
case strings.HasPrefix(fn.Name.Name, "Benchmark"):
Expand Down Expand Up @@ -108,7 +110,59 @@ func lookup(pkg *types.Package, name string) []types.Object {
return ret
}

func checkExample(pass *analysis.Pass, fn *ast.FuncDecl) {
// This pattern is taken from /go/src/go/doc/example.go
var outputRe = regexp.MustCompile(`(?i)^[[:space:]]*(unordered )?output:`)

type commentMetadata struct {
isOutput bool
pos token.Pos
}

func checkExampleOutput(pass *analysis.Pass, fn *ast.FuncDecl, fileComments []*ast.CommentGroup) {
commentsInExample := []commentMetadata{}
numOutputs := 0

// Find the comment blocks that are in the example. These comments are
// guaranteed to be in order of appearance.
for _, cg := range fileComments {
if cg.Pos() < fn.Pos() {
continue
} else if cg.End() > fn.End() {
break
}

isOutput := outputRe.MatchString(cg.Text())
if isOutput {
numOutputs++
}

commentsInExample = append(commentsInExample, commentMetadata{
isOutput: isOutput,
pos: cg.Pos(),
})
}

// Change message based on whether there are multiple output comment blocks.
msg := "output comment block must be the last comment block"
if numOutputs > 1 {
msg = "there can only be one output comment block per example"
}

for i, cg := range commentsInExample {
// Check for output comments that are not the last comment in the example.
isLast := (i == len(commentsInExample)-1)
if cg.isOutput && !isLast {
pass.Report(
analysis.Diagnostic{
Pos: cg.pos,
Message: msg,
},
)
}
}
}

func checkExampleName(pass *analysis.Pass, fn *ast.FuncDecl) {
fnName := fn.Name.Name
if params := fn.Type.Params; len(params.List) != 0 {
pass.Reportf(fn.Pos(), "%s should be niladic", fnName)
Expand Down

0 comments on commit b182fde

Please sign in to comment.