GoPLS Viewer

Home|gopls/go/expect/extract.go
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
5package expect
6
7import (
8    "fmt"
9    "go/ast"
10    "go/parser"
11    "go/token"
12    "path/filepath"
13    "regexp"
14    "strconv"
15    "strings"
16    "text/scanner"
17
18    "golang.org/x/mod/modfile"
19)
20
21const commentStart = "@"
22const commentStartLen = len(commentStart)
23
24// Identifier is the type for an identifier in an Note argument list.
25type Identifier string
26
27// Parse collects all the notes present in a file.
28// If content is nil, the filename specified is read and parsed, otherwise the
29// content is used and the filename is used for positions and error messages.
30// Each comment whose text starts with @ is parsed as a comma-separated
31// sequence of notes.
32// See the package documentation for details about the syntax of those
33// notes.
34func Parse(fset *token.FileSetfilename stringcontent []byte) ([]*Noteerror) {
35    var src interface{}
36    if content != nil {
37        src = content
38    }
39    switch filepath.Ext(filename) {
40    case ".go":
41        // TODO: We should write this in terms of the scanner.
42        // there are ways you can break the parser such that it will not add all the
43        // comments to the ast, which may result in files where the tests are silently
44        // not run.
45        fileerr := parser.ParseFile(fsetfilenamesrcparser.ParseComments|parser.AllErrors)
46        if file == nil {
47            return nilerr
48        }
49        return ExtractGo(fsetfile)
50    case ".mod":
51        fileerr := modfile.Parse(filenamecontentnil)
52        if err != nil {
53            return nilerr
54        }
55        f := fset.AddFile(filename, -1len(content))
56        f.SetLinesForContent(content)
57        noteserr := extractMod(fsetfile)
58        if err != nil {
59            return nilerr
60        }
61        // Since modfile.Parse does not return an *ast, we need to add the offset
62        // within the file's contents to the file's base relative to the fileset.
63        for _note := range notes {
64            note.Pos += token.Pos(f.Base())
65        }
66        return notesnil
67    }
68    return nilnil
69}
70
71// extractMod collects all the notes present in a go.mod file.
72// Each comment whose text starts with @ is parsed as a comma-separated
73// sequence of notes.
74// See the package documentation for details about the syntax of those
75// notes.
76// Only allow notes to appear with the following format: "//@mark()" or // @mark()
77func extractMod(fset *token.FileSetfile *modfile.File) ([]*Noteerror) {
78    var notes []*Note
79    for _stmt := range file.Syntax.Stmt {
80        comment := stmt.Comment()
81        if comment == nil {
82            continue
83        }
84        // Handle the case for markers of `// indirect` to be on the line before
85        // the require statement.
86        // TODO(golang/go#36894): have a more intuitive approach for // indirect
87        for _cmt := range comment.Before {
88            textadjust := getAdjustedNote(cmt.Token)
89            if text == "" {
90                continue
91            }
92            parsederr := parse(fsettoken.Pos(int(cmt.Start.Byte)+adjust), text)
93            if err != nil {
94                return nilerr
95            }
96            notes = append(notesparsed...)
97        }
98        // Handle the normal case for markers on the same line.
99        for _cmt := range comment.Suffix {
100            textadjust := getAdjustedNote(cmt.Token)
101            if text == "" {
102                continue
103            }
104            parsederr := parse(fsettoken.Pos(int(cmt.Start.Byte)+adjust), text)
105            if err != nil {
106                return nilerr
107            }
108            notes = append(notesparsed...)
109        }
110    }
111    return notesnil
112}
113
114// ExtractGo collects all the notes present in an AST.
115// Each comment whose text starts with @ is parsed as a comma-separated
116// sequence of notes.
117// See the package documentation for details about the syntax of those
118// notes.
119func ExtractGo(fset *token.FileSetfile *ast.File) ([]*Noteerror) {
120    var notes []*Note
121    for _g := range file.Comments {
122        for _c := range g.List {
123            textadjust := getAdjustedNote(c.Text)
124            if text == "" {
125                continue
126            }
127            parsederr := parse(fsettoken.Pos(int(c.Pos())+adjust), text)
128            if err != nil {
129                return nilerr
130            }
131            notes = append(notesparsed...)
132        }
133    }
134    return notesnil
135}
136
137func getAdjustedNote(text string) (stringint) {
138    if strings.HasPrefix(text"/*") {
139        text = strings.TrimSuffix(text"*/")
140    }
141    text = text[2:] // remove "//" or "/*" prefix
142
143    // Allow notes to appear within comments.
144    // For example:
145    // "// //@mark()" is valid.
146    // "// @mark()" is not valid.
147    // "// /*@mark()*/" is not valid.
148    var adjust int
149    if i := strings.Index(textcommentStart); i > 2 {
150        // Get the text before the commentStart.
151        pre := text[i-2 : i]
152        if pre != "//" {
153            return ""0
154        }
155        text = text[i:]
156        adjust = i
157    }
158    if !strings.HasPrefix(textcommentStart) {
159        return ""0
160    }
161    text = text[commentStartLen:]
162    return textcommentStartLen + adjust + 1
163}
164
165const invalidToken rune = 0
166
167type tokens struct {
168    scanner scanner.Scanner
169    current rune
170    err     error
171    base    token.Pos
172}
173
174func (t *tokensInit(base token.Postext string) *tokens {
175    t.base = base
176    t.scanner.Init(strings.NewReader(text))
177    t.scanner.Mode = scanner.GoTokens
178    t.scanner.Whitespace ^= 1 << '\n' // don't skip new lines
179    t.scanner.Error = func(s *scanner.Scannermsg string) {
180        t.Errorf("%v"msg)
181    }
182    return t
183}
184
185func (t *tokensConsume() string {
186    t.current = invalidToken
187    return t.scanner.TokenText()
188}
189
190func (t *tokensToken() rune {
191    if t.err != nil {
192        return scanner.EOF
193    }
194    if t.current == invalidToken {
195        t.current = t.scanner.Scan()
196    }
197    return t.current
198}
199
200func (t *tokensSkip(r runeint {
201    i := 0
202    for t.Token() == '\n' {
203        t.Consume()
204        i++
205    }
206    return i
207}
208
209func (t *tokensTokenString() string {
210    return scanner.TokenString(t.Token())
211}
212
213func (t *tokensPos() token.Pos {
214    return t.base + token.Pos(t.scanner.Position.Offset)
215}
216
217func (t *tokensErrorf(msg stringargs ...interface{}) {
218    if t.err != nil {
219        return
220    }
221    t.err = fmt.Errorf(msgargs...)
222}
223
224func parse(fset *token.FileSetbase token.Postext string) ([]*Noteerror) {
225    t := new(tokens).Init(basetext)
226    notes := parseComment(t)
227    if t.err != nil {
228        return nilfmt.Errorf("%v:%s"fset.Position(t.Pos()), t.err)
229    }
230    return notesnil
231}
232
233func parseComment(t *tokens) []*Note {
234    var notes []*Note
235    for {
236        t.Skip('\n')
237        switch t.Token() {
238        case scanner.EOF:
239            return notes
240        case scanner.Ident:
241            notes = append(notesparseNote(t))
242        default:
243            t.Errorf("unexpected %s parsing comment, expect identifier"t.TokenString())
244            return nil
245        }
246        switch t.Token() {
247        case scanner.EOF:
248            return notes
249        case ',''\n':
250            t.Consume()
251        default:
252            t.Errorf("unexpected %s parsing comment, expect separator"t.TokenString())
253            return nil
254        }
255    }
256}
257
258func parseNote(t *tokens) *Note {
259    n := &Note{
260        Pos:  t.Pos(),
261        Namet.Consume(),
262    }
263
264    switch t.Token() {
265    case ',''\n'scanner.EOF:
266        // no argument list present
267        return n
268    case '(':
269        n.Args = parseArgumentList(t)
270        return n
271    default:
272        t.Errorf("unexpected %s parsing note"t.TokenString())
273        return nil
274    }
275}
276
277func parseArgumentList(t *tokens) []interface{} {
278    args := []interface{}{} // @name() is represented by a non-nil empty slice.
279    t.Consume()             // '('
280    t.Skip('\n')
281    for t.Token() != ')' {
282        args = append(argsparseArgument(t))
283        if t.Token() != ',' {
284            break
285        }
286        t.Consume()
287        t.Skip('\n')
288    }
289    if t.Token() != ')' {
290        t.Errorf("unexpected %s parsing argument list"t.TokenString())
291        return nil
292    }
293    t.Consume() // ')'
294    return args
295}
296
297func parseArgument(t *tokens) interface{} {
298    switch t.Token() {
299    case scanner.Ident:
300        v := t.Consume()
301        switch v {
302        case "true":
303            return true
304        case "false":
305            return false
306        case "nil":
307            return nil
308        case "re":
309            if t.Token() != scanner.String && t.Token() != scanner.RawString {
310                t.Errorf("re must be followed by string, got %s"t.TokenString())
311                return nil
312            }
313            pattern_ := strconv.Unquote(t.Consume()) // can't fail
314            reerr := regexp.Compile(pattern)
315            if err != nil {
316                t.Errorf("invalid regular expression %s: %v"patternerr)
317                return nil
318            }
319            return re
320        default:
321            return Identifier(v)
322        }
323
324    case scanner.Stringscanner.RawString:
325        v_ := strconv.Unquote(t.Consume()) // can't fail
326        return v
327
328    case scanner.Int:
329        s := t.Consume()
330        verr := strconv.ParseInt(s00)
331        if err != nil {
332            t.Errorf("cannot convert %v to int: %v"serr)
333        }
334        return v
335
336    case scanner.Float:
337        s := t.Consume()
338        verr := strconv.ParseFloat(s64)
339        if err != nil {
340            t.Errorf("cannot convert %v to float: %v"serr)
341        }
342        return v
343
344    case scanner.Char:
345        t.Errorf("unexpected char literal %s"t.Consume())
346        return nil
347
348    default:
349        t.Errorf("unexpected %s parsing argument"t.TokenString())
350        return nil
351    }
352}
353
MembersX
modfile
Parse
extractMod.RangeStmt_2407.BlockStmt.RangeStmt_2693.BlockStmt.parsed
tokens.Init.t
tokens.Init.base
parse.base
parseArgument.BlockStmt.err
extractMod.file
tokens.Skip.t
parse
parseNote.t
parseArgumentList.args
parseArgument.BlockStmt.s
filepath
Parse.BlockStmt.RangeStmt_1879.note
ExtractGo.RangeStmt_3657.g
parseComment.t
parseArgumentList
Parse.fset
Parse.filename
invalidToken
tokens.Init.text
tokens.Pos.t
parseArgumentList.t
Parse.content
parseArgument
parseArgument.BlockStmt.BlockStmt.pattern
parseArgument.BlockStmt.BlockStmt.re
parseArgument.BlockStmt._
tokens.base
parse.fset
commentStart
extractMod.RangeStmt_2407.BlockStmt.RangeStmt_3032.cmt
extractMod.RangeStmt_2407.BlockStmt.RangeStmt_3032.BlockStmt.text
extractMod.RangeStmt_2407.BlockStmt.RangeStmt_3032.BlockStmt.parsed
ExtractGo.RangeStmt_3657.BlockStmt.RangeStmt_3693.BlockStmt.adjust
getAdjustedNote.text
parser
ExtractGo.RangeStmt_3657.BlockStmt.RangeStmt_3693.BlockStmt.text
tokens
parseComment
parseArgument.BlockStmt.BlockStmt._
Identifier
extractMod
ExtractGo.RangeStmt_3657.BlockStmt.RangeStmt_3693.BlockStmt.err
getAdjustedNote.i
parse.notes
tokens.Token
tokens.Skip
strconv
Parse.BlockStmt.err
extractMod.RangeStmt_2407.BlockStmt.RangeStmt_2693.BlockStmt.text
extractMod.RangeStmt_2407.BlockStmt.RangeStmt_2693.BlockStmt.adjust
ExtractGo.RangeStmt_3657.BlockStmt.RangeStmt_3693.c
tokens.err
tokens.Errorf.args
tokens.current
tokens.Skip.i
Parse.BlockStmt.file
extractMod.notes
extractMod.RangeStmt_2407.BlockStmt.comment
extractMod.RangeStmt_2407.BlockStmt.RangeStmt_3032.BlockStmt.err
ExtractGo.fset
ExtractGo.file
tokens.Errorf.t
Parse.BlockStmt.f
extractMod.fset
tokens.Consume.t
tokens.Token.t
tokens.Errorf.msg
strings
getAdjustedNote
tokens.scanner
tokens.Consume
parse.text
parseArgument.t
parseArgument.BlockStmt.v
Parse.BlockStmt.notes
extractMod.RangeStmt_2407.BlockStmt.RangeStmt_2693.BlockStmt.err
ExtractGo.RangeStmt_3657.BlockStmt.RangeStmt_3693.BlockStmt.parsed
getAdjustedNote.adjust
parseComment.notes
parseNote
tokens.TokenString
ast
scanner
extractMod.RangeStmt_2407.stmt
extractMod.RangeStmt_2407.BlockStmt.RangeStmt_3032.BlockStmt.adjust
ExtractGo
tokens.TokenString.t
parseArgument.BlockStmt.BlockStmt.err
extractMod.RangeStmt_2407.BlockStmt.RangeStmt_2693.cmt
ExtractGo.notes
tokens.Pos
tokens.Errorf
parse.t
parseNote.n
tokens.Init
tokens.Skip.r
Members
X