gopkg.in/alecthomas/gometalinter.v3@v3.0.0/_linters/src/golang.org/x/tools/go/expect/extract.go (about)

     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  package expect
     6  
     7  import (
     8  	"fmt"
     9  	"go/ast"
    10  	"go/parser"
    11  	"go/token"
    12  	"regexp"
    13  	"strconv"
    14  	"strings"
    15  	"text/scanner"
    16  )
    17  
    18  const (
    19  	commentStart = "@"
    20  )
    21  
    22  // Identifier is the type for an identifier in an Note argument list.
    23  type Identifier string
    24  
    25  // Parse collects all the notes present in a file.
    26  // If content is nil, the filename specified is read and parsed, otherwise the
    27  // content is used and the filename is used for positions and error messages.
    28  // Each comment whose text starts with @ is parsed as a comma-separated
    29  // sequence of notes.
    30  // See the package documentation for details about the syntax of those
    31  // notes.
    32  func Parse(fset *token.FileSet, filename string, content []byte) ([]*Note, error) {
    33  	var src interface{}
    34  	if content != nil {
    35  		src = content
    36  	}
    37  	// TODO: We should write this in terms of the scanner.
    38  	// there are ways you can break the parser such that it will not add all the
    39  	// comments to the ast, which may result in files where the tests are silently
    40  	// not run.
    41  	file, err := parser.ParseFile(fset, filename, src, parser.ParseComments)
    42  	if file == nil {
    43  		return nil, err
    44  	}
    45  	return Extract(fset, file)
    46  }
    47  
    48  // Extract collects all the notes present in an AST.
    49  // Each comment whose text starts with @ is parsed as a comma-separated
    50  // sequence of notes.
    51  // See the package documentation for details about the syntax of those
    52  // notes.
    53  func Extract(fset *token.FileSet, file *ast.File) ([]*Note, error) {
    54  	var notes []*Note
    55  	for _, g := range file.Comments {
    56  		for _, c := range g.List {
    57  			text := c.Text
    58  			if strings.HasPrefix(text, "/*") {
    59  				text = strings.TrimSuffix(text, "*/")
    60  			}
    61  			text = text[2:] // remove "//" or "/*" prefix
    62  			if !strings.HasPrefix(text, commentStart) {
    63  				continue
    64  			}
    65  			text = text[len(commentStart):]
    66  			parsed, err := parse(fset, c.Pos()+4, text)
    67  			if err != nil {
    68  				return nil, err
    69  			}
    70  			notes = append(notes, parsed...)
    71  		}
    72  	}
    73  	return notes, nil
    74  }
    75  
    76  const invalidToken rune = 0
    77  
    78  type tokens struct {
    79  	scanner scanner.Scanner
    80  	current rune
    81  	err     error
    82  	base    token.Pos
    83  }
    84  
    85  func (t *tokens) Init(base token.Pos, text string) *tokens {
    86  	t.base = base
    87  	t.scanner.Init(strings.NewReader(text))
    88  	t.scanner.Mode = scanner.GoTokens
    89  	t.scanner.Whitespace ^= 1 << '\n' // don't skip new lines
    90  	t.scanner.Error = func(s *scanner.Scanner, msg string) {
    91  		t.Errorf("%v", msg)
    92  	}
    93  	return t
    94  }
    95  
    96  func (t *tokens) Consume() string {
    97  	t.current = invalidToken
    98  	return t.scanner.TokenText()
    99  }
   100  
   101  func (t *tokens) Token() rune {
   102  	if t.err != nil {
   103  		return scanner.EOF
   104  	}
   105  	if t.current == invalidToken {
   106  		t.current = t.scanner.Scan()
   107  	}
   108  	return t.current
   109  }
   110  
   111  func (t *tokens) Skip(r rune) int {
   112  	i := 0
   113  	for t.Token() == '\n' {
   114  		t.Consume()
   115  		i++
   116  	}
   117  	return i
   118  }
   119  
   120  func (t *tokens) TokenString() string {
   121  	return scanner.TokenString(t.Token())
   122  }
   123  
   124  func (t *tokens) Pos() token.Pos {
   125  	return t.base + token.Pos(t.scanner.Position.Offset)
   126  }
   127  
   128  func (t *tokens) Errorf(msg string, args ...interface{}) {
   129  	if t.err != nil {
   130  		return
   131  	}
   132  	t.err = fmt.Errorf(msg, args...)
   133  }
   134  
   135  func parse(fset *token.FileSet, base token.Pos, text string) ([]*Note, error) {
   136  	t := new(tokens).Init(base, text)
   137  	notes := parseComment(t)
   138  	if t.err != nil {
   139  		return nil, fmt.Errorf("%v:%s", fset.Position(t.Pos()), t.err)
   140  	}
   141  	return notes, nil
   142  }
   143  
   144  func parseComment(t *tokens) []*Note {
   145  	var notes []*Note
   146  	for {
   147  		t.Skip('\n')
   148  		switch t.Token() {
   149  		case scanner.EOF:
   150  			return notes
   151  		case scanner.Ident:
   152  			notes = append(notes, parseNote(t))
   153  		default:
   154  			t.Errorf("unexpected %s parsing comment, expect identifier", t.TokenString())
   155  			return nil
   156  		}
   157  		switch t.Token() {
   158  		case scanner.EOF:
   159  			return notes
   160  		case ',', '\n':
   161  			t.Consume()
   162  		default:
   163  			t.Errorf("unexpected %s parsing comment, expect separator", t.TokenString())
   164  			return nil
   165  		}
   166  	}
   167  }
   168  
   169  func parseNote(t *tokens) *Note {
   170  	n := &Note{
   171  		Pos:  t.Pos(),
   172  		Name: t.Consume(),
   173  	}
   174  
   175  	switch t.Token() {
   176  	case ',', '\n', scanner.EOF:
   177  		// no argument list present
   178  		return n
   179  	case '(':
   180  		n.Args = parseArgumentList(t)
   181  		return n
   182  	default:
   183  		t.Errorf("unexpected %s parsing note", t.TokenString())
   184  		return nil
   185  	}
   186  }
   187  
   188  func parseArgumentList(t *tokens) []interface{} {
   189  	args := []interface{}{} // @name() is represented by a non-nil empty slice.
   190  	t.Consume()             // '('
   191  	t.Skip('\n')
   192  	for t.Token() != ')' {
   193  		args = append(args, parseArgument(t))
   194  		if t.Token() != ',' {
   195  			break
   196  		}
   197  		t.Consume()
   198  		t.Skip('\n')
   199  	}
   200  	if t.Token() != ')' {
   201  		t.Errorf("unexpected %s parsing argument list", t.TokenString())
   202  		return nil
   203  	}
   204  	t.Consume() // ')'
   205  	return args
   206  }
   207  
   208  func parseArgument(t *tokens) interface{} {
   209  	switch t.Token() {
   210  	case scanner.Ident:
   211  		v := t.Consume()
   212  		switch v {
   213  		case "true":
   214  			return true
   215  		case "false":
   216  			return false
   217  		case "nil":
   218  			return nil
   219  		case "re":
   220  			if t.Token() != scanner.String && t.Token() != scanner.RawString {
   221  				t.Errorf("re must be followed by string, got %s", t.TokenString())
   222  				return nil
   223  			}
   224  			pattern, _ := strconv.Unquote(t.Consume()) // can't fail
   225  			re, err := regexp.Compile(pattern)
   226  			if err != nil {
   227  				t.Errorf("invalid regular expression %s: %v", pattern, err)
   228  				return nil
   229  			}
   230  			return re
   231  		default:
   232  			return Identifier(v)
   233  		}
   234  
   235  	case scanner.String, scanner.RawString:
   236  		v, _ := strconv.Unquote(t.Consume()) // can't fail
   237  		return v
   238  
   239  	case scanner.Int:
   240  		s := t.Consume()
   241  		v, err := strconv.ParseInt(s, 0, 0)
   242  		if err != nil {
   243  			t.Errorf("cannot convert %v to int: %v", s, err)
   244  		}
   245  		return v
   246  
   247  	case scanner.Float:
   248  		s := t.Consume()
   249  		v, err := strconv.ParseFloat(s, 64)
   250  		if err != nil {
   251  			t.Errorf("cannot convert %v to float: %v", s, err)
   252  		}
   253  		return v
   254  
   255  	case scanner.Char:
   256  		t.Errorf("unexpected char literal %s", t.Consume())
   257  		return nil
   258  
   259  	default:
   260  		t.Errorf("unexpected %s parsing argument", t.TokenString())
   261  		return nil
   262  	}
   263  }