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