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 <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>
diff --git a/go/analysis/passes/tests/testdata/src/a/a_test.go b/go/analysis/passes/tests/testdata/src/a/a_test.go
index 67bfda7..e38184a 100644
--- a/go/analysis/passes/tests/testdata/src/a/a_test.go
+++ b/go/analysis/passes/tests/testdata/src/a/a_test.go
@@ -53,6 +53,46 @@
 
 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.
diff --git a/go/analysis/passes/tests/tests.go b/go/analysis/passes/tests/tests.go
index 8232276..570ad5c 100644
--- a/go/analysis/passes/tests/tests.go
+++ b/go/analysis/passes/tests/tests.go
@@ -8,7 +8,9 @@
 
 import (
 	"go/ast"
+	"go/token"
 	"go/types"
+	"regexp"
 	"strings"
 	"unicode"
 	"unicode/utf8"
@@ -42,10 +44,10 @@
 				// 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"):
@@ -108,7 +110,59 @@
 	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)