| // 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(f.LineStart(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 |
| } |
| |
| func lineEnd(f *token.File, line int) token.Pos { |
| if line >= f.LineCount() { |
| return token.Pos(f.Base() + f.Size()) |
| } |
| return f.LineStart(line + 1) |
| } |