⛈ Mood: Enjoying the thunderstorm.

🎵 Soundtrack: “No Lyrics - Folk” Playlist

☀️ Morning (Lack of) Progress

Started today by wrangling expense reports for another hour. Wow she has a lot of expense reports you might think. But no, these are all the same expenses report that I keep trying to submit that keep getting rejected. Also an all hands meeting. Needless to say, no golang work got done.

📚 Reading Github Issues

I started this afternoon by reading the 11 new github issues. From this issue I learned that go playground lets you talk to the loopback interface to test networking packages. Cool!

✏️ Updating the Contributors Guide

As I mentioned last week, one thing I wish was in the contributors guide was a way to run the tests for individual packages. You can’t just run them like normal go tests. An easy PR and win for all!

Initially I spent most of my time trying to figure out which repo the website is in. Turns out it is: https://github.com/golang/website. It was easy to follow the readme and run the server. When I ran it I realized…..there is already a section on how to run individual packages tests: https://golang.org/doc/contribute#quick_test. I had just missed it. And to make it worse, I was doing it wrong. Well, at least I learned something. No quick and easy contribution for me then.

👀 Searching for Bad Examples

Next I turned my mind back to the issue I filed about false positives in examples when there is a comment after the output comment block. Technically you are not allowed to have comments after the output comment block, but since everything passes when you do have comments after the output comment block it is easy to miss. I found one example of this a couple days ago. But now I am curious if there are more. Is this worth writing a go vet warning for?

Time to figure out how to parse things.

🎥 I watched the following two videos on scanning and parsing in golang

One of the most important things I learned was about the go-spew package which is an easy way to print (spew) ast nodes.

I was able to get a prototype working that:

  • grabs all example tests in a file
  • grabs all comments
  • finds the comments for each test
  • checks if each comment is (1) an output comment and if it is (2) the last comment in the test
    • if it IS an output comment and IS NOT the last comment in a test
      • then it prints a warning

⚠️ Warning: unrefactored, untested code ahead.

package main

import (
	"fmt"
	"go/ast"
	"go/doc"
	"go/parser"
	"go/token"
	"log"
	"regexp"
)

var outputPrefix = `(?i)^[[:space:]]*(unordered )?output:`

type CommentForExample struct {
	Index    int
	IsOutput bool
	Text     string
}

func main() {
	fset := token.NewFileSet()
	node, err := parser.ParseFile(fset, "examples_test.go", nil, parser.ParseComments)
	if err != nil {
		log.Fatal(err)
	}
	examples := doc.Examples(node)

	for _, e := range examples {
		b := e.Code

		pos, end := b.Pos(), b.End()

		numberOfCommentBlocks := 0
		comments := []CommentForExample{}
		for _, cg := range e.Comments {
			if cg.Pos() < pos {
				continue
			} else if cg.End() > end {
				break
			} else {
				_, _, ok := exampleOutput(cg)
				com := CommentForExample{
					Index:    numberOfCommentBlocks,
					IsOutput: ok,
					Text:     cg.Text(),
				}
				comments = append(comments, com)
				numberOfCommentBlocks++
			}
		}
		for _, c := range comments {
			if c.IsOutput && c.Index != (numberOfCommentBlocks-1) {
				fmt.Printf("OH NO! Test: %q\nDangerous Comment: %q\n\n", e.Name, c.Text)
			}
		}
	}
}

func exampleOutput(comment *ast.CommentGroup) (output string, unordered, ok bool) {
	// test that it begins with the correct prefix
	text := comment.Text()
	matched, err := regexp.MatchString(outputPrefix, text)
	if err != nil {
		log.Fatal(err)
	}
	if matched {
		return text, false, true
	}
	return "", false, false // no suitable comment found

}

One weird this is that the type Example has a value Comments. But this value seems to be the comments for the ENTIRE file, not just for that example. That why I needed to do the position checking. I heavily cribbed that part from lastComment, which returns the last commentgroup for an example. Yesterday when I saw that function I was wondering why they had to do all of the position checking and now I understand. 🤔 I don’t understand why it was built this way though.

So here is my example test file:

package language_test

import "fmt"

func ExampleMeow() {
	fmt.Println("meow")

	// Output:
	// meow
}

func ExampleBark() {
	fmt.Println("bark")

	// Output:
	// bark
	// bark

	// oops
}

And when I run my program and point it at that file, I get the following output:

$ go run main.go
OH NO! Test: "Bark"
Dangerous Comment: "Output:\nbark\nbark\n"

🎉Yay! It worked! Now I just need to figure out how to parse all of the files in a directory. Sounds solvable, but I don’t have time for it today.

👋 See you all Monday I’m off on vacation! 🏔 🚴‍♀️🌊