150 lines
4.9 KiB
Go
150 lines
4.9 KiB
Go
// Copyright 2018 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
/*
|
|
Package expect provides support for interpreting structured comments in Go
|
|
source code as test expectations.
|
|
|
|
This is primarily intended for writing tests of things that process Go source
|
|
files, although it does not directly depend on the testing package.
|
|
|
|
Collect notes with the Extract or Parse functions, and use the
|
|
MatchBefore function to find matches within the lines the comments were on.
|
|
|
|
The interpretation of the notes depends on the application.
|
|
For example, the test suite for a static checking tool might
|
|
use a @diag note to indicate an expected diagnostic:
|
|
|
|
fmt.Printf("%s", 1) //@ diag("%s wants a string, got int")
|
|
|
|
By contrast, the test suite for a source code navigation tool
|
|
might use notes to indicate the positions of features of
|
|
interest, the actions to be performed by the test,
|
|
and their expected outcomes:
|
|
|
|
var x = 1 //@ x_decl
|
|
...
|
|
print(x) //@ definition("x", x_decl)
|
|
print(x) //@ typeof("x", "int")
|
|
|
|
|
|
Note comment syntax
|
|
|
|
Note comments always start with the special marker @, which must be the
|
|
very first character after the comment opening pair, so //@ or /*@ with no
|
|
spaces.
|
|
|
|
This is followed by a comma separated list of notes.
|
|
|
|
A note always starts with an identifier, which is optionally followed by an
|
|
argument list. The argument list is surrounded with parentheses and contains a
|
|
comma-separated list of arguments.
|
|
The empty parameter list and the missing parameter list are distinguishable if
|
|
needed; they result in a nil or an empty list in the Args parameter respectively.
|
|
|
|
Arguments are either identifiers or literals.
|
|
The literals supported are the basic value literals, of string, float, integer
|
|
true, false or nil. All the literals match the standard go conventions, with
|
|
all bases of integers, and both quote and backtick strings.
|
|
There is one extra literal type, which is a string literal preceded by the
|
|
identifier "re" which is compiled to a regular expression.
|
|
*/
|
|
package expect
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"go/token"
|
|
"regexp"
|
|
)
|
|
|
|
// Note is a parsed note from an expect comment.
|
|
// It knows the position of the start of the comment, and the name and
|
|
// arguments that make up the note.
|
|
type Note struct {
|
|
Pos token.Pos // The position at which the note identifier appears
|
|
Name string // the name associated with the note
|
|
Args []interface{} // the arguments for the note
|
|
}
|
|
|
|
// ReadFile is the type of a function that can provide file contents for a
|
|
// given filename.
|
|
// This is used in MatchBefore to look up the content of the file in order to
|
|
// find the line to match the pattern against.
|
|
type ReadFile func(filename string) ([]byte, error)
|
|
|
|
// MatchBefore attempts to match a pattern in the line before the supplied pos.
|
|
// It uses the FileSet and the ReadFile to work out the contents of the line
|
|
// that end is part of, and then matches the pattern against the content of the
|
|
// start of that line up to the supplied position.
|
|
// The pattern may be either a simple string, []byte or a *regexp.Regexp.
|
|
// MatchBefore returns the range of the line that matched the pattern, and
|
|
// invalid positions if there was no match, or an error if the line could not be
|
|
// found.
|
|
func MatchBefore(fset *token.FileSet, readFile ReadFile, end token.Pos, pattern interface{}) (token.Pos, token.Pos, error) {
|
|
f := fset.File(end)
|
|
content, err := readFile(f.Name())
|
|
if err != nil {
|
|
return token.NoPos, token.NoPos, fmt.Errorf("invalid file: %v", err)
|
|
}
|
|
position := f.Position(end)
|
|
startOffset := f.Offset(lineStart(f, position.Line))
|
|
endOffset := f.Offset(end)
|
|
line := content[startOffset:endOffset]
|
|
matchStart, matchEnd := -1, -1
|
|
switch pattern := pattern.(type) {
|
|
case string:
|
|
bytePattern := []byte(pattern)
|
|
matchStart = bytes.Index(line, bytePattern)
|
|
if matchStart >= 0 {
|
|
matchEnd = matchStart + len(bytePattern)
|
|
}
|
|
case []byte:
|
|
matchStart = bytes.Index(line, pattern)
|
|
if matchStart >= 0 {
|
|
matchEnd = matchStart + len(pattern)
|
|
}
|
|
case *regexp.Regexp:
|
|
match := pattern.FindIndex(line)
|
|
if len(match) > 0 {
|
|
matchStart = match[0]
|
|
matchEnd = match[1]
|
|
}
|
|
}
|
|
if matchStart < 0 {
|
|
return token.NoPos, token.NoPos, nil
|
|
}
|
|
return f.Pos(startOffset + matchStart), f.Pos(startOffset + matchEnd), nil
|
|
}
|
|
|
|
// this functionality was borrowed from the analysisutil package
|
|
func lineStart(f *token.File, line int) token.Pos {
|
|
// Use binary search to find the start offset of this line.
|
|
//
|
|
// TODO(adonovan): eventually replace this function with the
|
|
// simpler and more efficient (*go/token.File).LineStart, added
|
|
// in go1.12.
|
|
|
|
min := 0 // inclusive
|
|
max := f.Size() // exclusive
|
|
for {
|
|
offset := (min + max) / 2
|
|
pos := f.Pos(offset)
|
|
posn := f.Position(pos)
|
|
if posn.Line == line {
|
|
return pos - (token.Pos(posn.Column) - 1)
|
|
}
|
|
|
|
if min+1 >= max {
|
|
return token.NoPos
|
|
}
|
|
|
|
if posn.Line < line {
|
|
min = offset
|
|
} else {
|
|
max = offset
|
|
}
|
|
}
|
|
}
|