1// Copyright 2018 The Go Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5/* 6Package expect provides support for interpreting structured comments in Go 7source code as test expectations. 8 9This is primarily intended for writing tests of things that process Go source 10files, although it does not directly depend on the testing package. 11 12Collect notes with the Extract or Parse functions, and use the 13MatchBefore function to find matches within the lines the comments were on. 14 15The interpretation of the notes depends on the application. 16For example, the test suite for a static checking tool might 17use a @diag note to indicate an expected diagnostic: 18 19 fmt.Printf("%s", 1) //@ diag("%s wants a string, got int") 20 21By contrast, the test suite for a source code navigation tool 22might use notes to indicate the positions of features of 23interest, the actions to be performed by the test, 24and their expected outcomes: 25 26 var x = 1 //@ x_decl 27 ... 28 print(x) //@ definition("x", x_decl) 29 print(x) //@ typeof("x", "int") 30 31 32Note comment syntax 33 34Note comments always start with the special marker @, which must be the 35very first character after the comment opening pair, so //@ or /*@ with no 36spaces. 37 38This is followed by a comma separated list of notes. 39 40A note always starts with an identifier, which is optionally followed by an 41argument list. The argument list is surrounded with parentheses and contains a 42comma-separated list of arguments. 43The empty parameter list and the missing parameter list are distinguishable if 44needed; they result in a nil or an empty list in the Args parameter respectively. 45 46Arguments are either identifiers or literals. 47The literals supported are the basic value literals, of string, float, integer 48true, false or nil. All the literals match the standard go conventions, with 49all bases of integers, and both quote and backtick strings. 50There is one extra literal type, which is a string literal preceded by the 51identifier "re" which is compiled to a regular expression. 52*/ 53package expect 54 55import ( 56 "bytes" 57 "fmt" 58 "go/token" 59 "regexp" 60) 61 62// Note is a parsed note from an expect comment. 63// It knows the position of the start of the comment, and the name and 64// arguments that make up the note. 65type Note struct { 66 Pos token.Pos // The position at which the note identifier appears 67 Name string // the name associated with the note 68 Args []interface{} // the arguments for the note 69} 70 71// ReadFile is the type of a function that can provide file contents for a 72// given filename. 73// This is used in MatchBefore to look up the content of the file in order to 74// find the line to match the pattern against. 75type ReadFile func(filename string) ([]byte, error) 76 77// MatchBefore attempts to match a pattern in the line before the supplied pos. 78// It uses the FileSet and the ReadFile to work out the contents of the line 79// that end is part of, and then matches the pattern against the content of the 80// start of that line up to the supplied position. 81// The pattern may be either a simple string, []byte or a *regexp.Regexp. 82// MatchBefore returns the range of the line that matched the pattern, and 83// invalid positions if there was no match, or an error if the line could not be 84// found. 85func MatchBefore(fset *token.FileSet, readFile ReadFile, end token.Pos, pattern interface{}) (token.Pos, token.Pos, error) { 86 f := fset.File(end) 87 content, err := readFile(f.Name()) 88 if err != nil { 89 return token.NoPos, token.NoPos, fmt.Errorf("invalid file: %v", err) 90 } 91 position := f.Position(end) 92 startOffset := f.Offset(f.LineStart(position.Line)) 93 endOffset := f.Offset(end) 94 line := content[startOffset:endOffset] 95 matchStart, matchEnd := -1, -1 96 switch pattern := pattern.(type) { 97 case string: 98 bytePattern := []byte(pattern) 99 matchStart = bytes.Index(line, bytePattern) 100 if matchStart >= 0 { 101 matchEnd = matchStart + len(bytePattern) 102 } 103 case []byte: 104 matchStart = bytes.Index(line, pattern) 105 if matchStart >= 0 { 106 matchEnd = matchStart + len(pattern) 107 } 108 case *regexp.Regexp: 109 match := pattern.FindIndex(line) 110 if len(match) > 0 { 111 matchStart = match[0] 112 matchEnd = match[1] 113 } 114 } 115 if matchStart < 0 { 116 return token.NoPos, token.NoPos, nil 117 } 118 return f.Pos(startOffset + matchStart), f.Pos(startOffset + matchEnd), nil 119} 120 121func lineEnd(f *token.File, line int) token.Pos { 122 if line >= f.LineCount() { 123 return token.Pos(f.Base() + f.Size()) 124 } 125 return f.LineStart(line + 1) 126} 127