// 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 import ( "fmt" "go/ast" "go/parser" "go/token" "regexp" "strconv" "strings" "text/scanner" ) const ( commentStart = "@" ) // Identifier is the type for an identifier in an Note argument list. type Identifier string // Parse collects all the notes present in a file. // If content is nil, the filename specified is read and parsed, otherwise the // content is used and the filename is used for positions and error messages. // Each comment whose text starts with @ is parsed as a comma-separated // sequence of notes. // See the package documentation for details about the syntax of those // notes. func Parse(fset *token.FileSet, filename string, content []byte) ([]*Note, error) { var src interface{} if content != nil { src = content } // TODO: We should write this in terms of the scanner. // there are ways you can break the parser such that it will not add all the // comments to the ast, which may result in files where the tests are silently // not run. file, err := parser.ParseFile(fset, filename, src, parser.ParseComments) if file == nil { return nil, err } return Extract(fset, file) } // Extract collects all the notes present in an AST. // Each comment whose text starts with @ is parsed as a comma-separated // sequence of notes. // See the package documentation for details about the syntax of those // notes. func Extract(fset *token.FileSet, file *ast.File) ([]*Note, error) { var notes []*Note for _, g := range file.Comments { for _, c := range g.List { text := c.Text if strings.HasPrefix(text, "/*") { text = strings.TrimSuffix(text, "*/") } text = text[2:] // remove "//" or "/*" prefix if !strings.HasPrefix(text, commentStart) { continue } text = text[len(commentStart):] parsed, err := parse(fset, c.Pos()+4, text) if err != nil { return nil, err } notes = append(notes, parsed...) } } return notes, nil } func parse(fset *token.FileSet, base token.Pos, text string) ([]*Note, error) { var scanErr error s := new(scanner.Scanner).Init(strings.NewReader(text)) s.Mode = scanner.GoTokens s.Error = func(s *scanner.Scanner, msg string) { scanErr = fmt.Errorf("%v:%s", fset.Position(base+token.Pos(s.Position.Offset)), msg) } notes, err := parseComment(s) if err != nil { return nil, fmt.Errorf("%v:%s", fset.Position(base+token.Pos(s.Position.Offset)), err) } if scanErr != nil { return nil, scanErr } for _, n := range notes { n.Pos += base } return notes, nil } func parseComment(s *scanner.Scanner) ([]*Note, error) { var notes []*Note for { n, err := parseNote(s) if err != nil { return nil, err } notes = append(notes, n) tok := s.Scan() switch tok { case ',': // continue case scanner.EOF: return notes, nil default: return nil, fmt.Errorf("unexpected %s parsing comment", scanner.TokenString(tok)) } } } func parseNote(s *scanner.Scanner) (*Note, error) { if tok := s.Scan(); tok != scanner.Ident { return nil, fmt.Errorf("expected identifier, got %s", scanner.TokenString(tok)) } n := &Note{ Pos: token.Pos(s.Position.Offset), Name: s.TokenText(), } switch s.Peek() { case ',', scanner.EOF: // no argument list present return n, nil case '(': // parse the argument list if tok := s.Scan(); tok != '(' { return nil, fmt.Errorf("expected ( got %s", scanner.TokenString(tok)) } // special case the empty argument list if s.Peek() == ')' { if tok := s.Scan(); tok != ')' { return nil, fmt.Errorf("expected ) got %s", scanner.TokenString(tok)) } n.Args = []interface{}{} // @name() is represented by a non-nil empty slice. return n, nil } // handle a normal argument list for { arg, err := parseArgument(s) if err != nil { return nil, err } n.Args = append(n.Args, arg) switch s.Peek() { case ')': if tok := s.Scan(); tok != ')' { return nil, fmt.Errorf("expected ) got %s", scanner.TokenString(tok)) } return n, nil case ',': if tok := s.Scan(); tok != ',' { return nil, fmt.Errorf("expected , got %s", scanner.TokenString(tok)) } // continue default: return nil, fmt.Errorf("unexpected %s parsing argument list", scanner.TokenString(s.Scan())) } } default: return nil, fmt.Errorf("unexpected %s parsing note", scanner.TokenString(s.Scan())) } } func parseArgument(s *scanner.Scanner) (interface{}, error) { tok := s.Scan() switch tok { case scanner.Ident: v := s.TokenText() switch v { case "true": return true, nil case "false": return false, nil case "nil": return nil, nil case "re": tok := s.Scan() switch tok { case scanner.String, scanner.RawString: pattern, _ := strconv.Unquote(s.TokenText()) // can't fail re, err := regexp.Compile(pattern) if err != nil { return nil, fmt.Errorf("invalid regular expression %s: %v", pattern, err) } return re, nil default: return nil, fmt.Errorf("re must be followed by string, got %s", scanner.TokenString(tok)) } default: return Identifier(v), nil } case scanner.String, scanner.RawString: v, _ := strconv.Unquote(s.TokenText()) // can't fail return v, nil case scanner.Int: v, err := strconv.ParseInt(s.TokenText(), 0, 0) if err != nil { return nil, fmt.Errorf("cannot convert %v to int: %v", s.TokenText(), err) } return v, nil case scanner.Float: v, err := strconv.ParseFloat(s.TokenText(), 64) if err != nil { return nil, fmt.Errorf("cannot convert %v to float: %v", s.TokenText(), err) } return v, nil case scanner.Char: return nil, fmt.Errorf("unexpected char literal %s", s.TokenText()) default: return nil, fmt.Errorf("unexpected %s parsing argument", scanner.TokenString(tok)) } }