Source file src/go/build/read.go

     1  // Copyright 2012 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 build
     6  
     7  import (
     8  	"bufio"
     9  	"bytes"
    10  	"errors"
    11  	"fmt"
    12  	"go/ast"
    13  	"go/parser"
    14  	"go/token"
    15  	"io"
    16  	"strconv"
    17  	"strings"
    18  	"unicode"
    19  	"unicode/utf8"
    20  )
    21  
    22  type importReader struct {
    23  	b    *bufio.Reader
    24  	buf  []byte
    25  	peek byte
    26  	err  error
    27  	eof  bool
    28  	nerr int
    29  	pos  token.Position
    30  }
    31  
    32  var bom = []byte{0xef, 0xbb, 0xbf}
    33  
    34  func newImportReader(name string, r io.Reader) *importReader {
    35  	b := bufio.NewReader(r)
    36  	// Remove leading UTF-8 BOM.
    37  	// Per https://golang.org/ref/spec#Source_code_representation:
    38  	// a compiler may ignore a UTF-8-encoded byte order mark (U+FEFF)
    39  	// if it is the first Unicode code point in the source text.
    40  	if leadingBytes, err := b.Peek(3); err == nil && bytes.Equal(leadingBytes, bom) {
    41  		b.Discard(3)
    42  	}
    43  	return &importReader{
    44  		b: b,
    45  		pos: token.Position{
    46  			Filename: name,
    47  			Line:     1,
    48  			Column:   1,
    49  		},
    50  	}
    51  }
    52  
    53  func isIdent(c byte) bool {
    54  	return 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9' || c == '_' || c >= utf8.RuneSelf
    55  }
    56  
    57  var (
    58  	errSyntax = errors.New("syntax error")
    59  	errNUL    = errors.New("unexpected NUL in input")
    60  )
    61  
    62  // syntaxError records a syntax error, but only if an I/O error has not already been recorded.
    63  func (r *importReader) syntaxError() {
    64  	if r.err == nil {
    65  		r.err = errSyntax
    66  	}
    67  }
    68  
    69  // readByte reads the next byte from the input, saves it in buf, and returns it.
    70  // If an error occurs, readByte records the error in r.err and returns 0.
    71  func (r *importReader) readByte() byte {
    72  	c, err := r.b.ReadByte()
    73  	if err == nil {
    74  		r.buf = append(r.buf, c)
    75  		if c == 0 {
    76  			err = errNUL
    77  		}
    78  	}
    79  	if err != nil {
    80  		if err == io.EOF {
    81  			r.eof = true
    82  		} else if r.err == nil {
    83  			r.err = err
    84  		}
    85  		c = 0
    86  	}
    87  	return c
    88  }
    89  
    90  // readByteNoBuf is like readByte but doesn't buffer the byte.
    91  // It exhausts r.buf before reading from r.b.
    92  func (r *importReader) readByteNoBuf() byte {
    93  	var c byte
    94  	var err error
    95  	if len(r.buf) > 0 {
    96  		c = r.buf[0]
    97  		r.buf = r.buf[1:]
    98  	} else {
    99  		c, err = r.b.ReadByte()
   100  		if err == nil && c == 0 {
   101  			err = errNUL
   102  		}
   103  	}
   104  
   105  	if err != nil {
   106  		if err == io.EOF {
   107  			r.eof = true
   108  		} else if r.err == nil {
   109  			r.err = err
   110  		}
   111  		return 0
   112  	}
   113  	r.pos.Offset++
   114  	if c == '\n' {
   115  		r.pos.Line++
   116  		r.pos.Column = 1
   117  	} else {
   118  		r.pos.Column++
   119  	}
   120  	return c
   121  }
   122  
   123  // peekByte returns the next byte from the input reader but does not advance beyond it.
   124  // If skipSpace is set, peekByte skips leading spaces and comments.
   125  func (r *importReader) peekByte(skipSpace bool) byte {
   126  	if r.err != nil {
   127  		if r.nerr++; r.nerr > 10000 {
   128  			panic("go/build: import reader looping")
   129  		}
   130  		return 0
   131  	}
   132  
   133  	// Use r.peek as first input byte.
   134  	// Don't just return r.peek here: it might have been left by peekByte(false)
   135  	// and this might be peekByte(true).
   136  	c := r.peek
   137  	if c == 0 {
   138  		c = r.readByte()
   139  	}
   140  	for r.err == nil && !r.eof {
   141  		if skipSpace {
   142  			// For the purposes of this reader, semicolons are never necessary to
   143  			// understand the input and are treated as spaces.
   144  			switch c {
   145  			case ' ', '\f', '\t', '\r', '\n', ';':
   146  				c = r.readByte()
   147  				continue
   148  
   149  			case '/':
   150  				c = r.readByte()
   151  				if c == '/' {
   152  					for c != '\n' && r.err == nil && !r.eof {
   153  						c = r.readByte()
   154  					}
   155  				} else if c == '*' {
   156  					var c1 byte
   157  					for (c != '*' || c1 != '/') && r.err == nil {
   158  						if r.eof {
   159  							r.syntaxError()
   160  						}
   161  						c, c1 = c1, r.readByte()
   162  					}
   163  				} else {
   164  					r.syntaxError()
   165  				}
   166  				c = r.readByte()
   167  				continue
   168  			}
   169  		}
   170  		break
   171  	}
   172  	r.peek = c
   173  	return r.peek
   174  }
   175  
   176  // nextByte is like peekByte but advances beyond the returned byte.
   177  func (r *importReader) nextByte(skipSpace bool) byte {
   178  	c := r.peekByte(skipSpace)
   179  	r.peek = 0
   180  	return c
   181  }
   182  
   183  var goEmbed = []byte("go:embed")
   184  
   185  // findEmbed advances the input reader to the next //go:embed comment.
   186  // It reports whether it found a comment.
   187  // (Otherwise it found an error or EOF.)
   188  func (r *importReader) findEmbed(first bool) bool {
   189  	// The import block scan stopped after a non-space character,
   190  	// so the reader is not at the start of a line on the first call.
   191  	// After that, each //go:embed extraction leaves the reader
   192  	// at the end of a line.
   193  	startLine := !first
   194  	var c byte
   195  	for r.err == nil && !r.eof {
   196  		c = r.readByteNoBuf()
   197  	Reswitch:
   198  		switch c {
   199  		default:
   200  			startLine = false
   201  
   202  		case '\n':
   203  			startLine = true
   204  
   205  		case ' ', '\t':
   206  			// leave startLine alone
   207  
   208  		case '"':
   209  			startLine = false
   210  			for r.err == nil {
   211  				if r.eof {
   212  					r.syntaxError()
   213  				}
   214  				c = r.readByteNoBuf()
   215  				if c == '\\' {
   216  					r.readByteNoBuf()
   217  					if r.err != nil {
   218  						r.syntaxError()
   219  						return false
   220  					}
   221  					continue
   222  				}
   223  				if c == '"' {
   224  					c = r.readByteNoBuf()
   225  					goto Reswitch
   226  				}
   227  			}
   228  			goto Reswitch
   229  
   230  		case '`':
   231  			startLine = false
   232  			for r.err == nil {
   233  				if r.eof {
   234  					r.syntaxError()
   235  				}
   236  				c = r.readByteNoBuf()
   237  				if c == '`' {
   238  					c = r.readByteNoBuf()
   239  					goto Reswitch
   240  				}
   241  			}
   242  
   243  		case '/':
   244  			c = r.readByteNoBuf()
   245  			switch c {
   246  			default:
   247  				startLine = false
   248  				goto Reswitch
   249  
   250  			case '*':
   251  				var c1 byte
   252  				for (c != '*' || c1 != '/') && r.err == nil {
   253  					if r.eof {
   254  						r.syntaxError()
   255  					}
   256  					c, c1 = c1, r.readByteNoBuf()
   257  				}
   258  				startLine = false
   259  
   260  			case '/':
   261  				if startLine {
   262  					// Try to read this as a //go:embed comment.
   263  					for i := range goEmbed {
   264  						c = r.readByteNoBuf()
   265  						if c != goEmbed[i] {
   266  							goto SkipSlashSlash
   267  						}
   268  					}
   269  					c = r.readByteNoBuf()
   270  					if c == ' ' || c == '\t' {
   271  						// Found one!
   272  						return true
   273  					}
   274  				}
   275  			SkipSlashSlash:
   276  				for c != '\n' && r.err == nil && !r.eof {
   277  					c = r.readByteNoBuf()
   278  				}
   279  				startLine = true
   280  			}
   281  		}
   282  	}
   283  	return false
   284  }
   285  
   286  // readKeyword reads the given keyword from the input.
   287  // If the keyword is not present, readKeyword records a syntax error.
   288  func (r *importReader) readKeyword(kw string) {
   289  	r.peekByte(true)
   290  	for i := 0; i < len(kw); i++ {
   291  		if r.nextByte(false) != kw[i] {
   292  			r.syntaxError()
   293  			return
   294  		}
   295  	}
   296  	if isIdent(r.peekByte(false)) {
   297  		r.syntaxError()
   298  	}
   299  }
   300  
   301  // readIdent reads an identifier from the input.
   302  // If an identifier is not present, readIdent records a syntax error.
   303  func (r *importReader) readIdent() {
   304  	c := r.peekByte(true)
   305  	if !isIdent(c) {
   306  		r.syntaxError()
   307  		return
   308  	}
   309  	for isIdent(r.peekByte(false)) {
   310  		r.peek = 0
   311  	}
   312  }
   313  
   314  // readString reads a quoted string literal from the input.
   315  // If an identifier is not present, readString records a syntax error.
   316  func (r *importReader) readString() {
   317  	switch r.nextByte(true) {
   318  	case '`':
   319  		for r.err == nil {
   320  			if r.nextByte(false) == '`' {
   321  				break
   322  			}
   323  			if r.eof {
   324  				r.syntaxError()
   325  			}
   326  		}
   327  	case '"':
   328  		for r.err == nil {
   329  			c := r.nextByte(false)
   330  			if c == '"' {
   331  				break
   332  			}
   333  			if r.eof || c == '\n' {
   334  				r.syntaxError()
   335  			}
   336  			if c == '\\' {
   337  				r.nextByte(false)
   338  			}
   339  		}
   340  	default:
   341  		r.syntaxError()
   342  	}
   343  }
   344  
   345  // readImport reads an import clause - optional identifier followed by quoted string -
   346  // from the input.
   347  func (r *importReader) readImport() {
   348  	c := r.peekByte(true)
   349  	if c == '.' {
   350  		r.peek = 0
   351  	} else if isIdent(c) {
   352  		r.readIdent()
   353  	}
   354  	r.readString()
   355  }
   356  
   357  // readComments is like io.ReadAll, except that it only reads the leading
   358  // block of comments in the file.
   359  func readComments(f io.Reader) ([]byte, error) {
   360  	r := newImportReader("", f)
   361  	r.peekByte(true)
   362  	if r.err == nil && !r.eof {
   363  		// Didn't reach EOF, so must have found a non-space byte. Remove it.
   364  		r.buf = r.buf[:len(r.buf)-1]
   365  	}
   366  	return r.buf, r.err
   367  }
   368  
   369  // readGoInfo expects a Go file as input and reads the file up to and including the import section.
   370  // It records what it learned in *info.
   371  // If info.fset is non-nil, readGoInfo parses the file and sets info.parsed, info.parseErr,
   372  // info.imports, info.embeds, and info.embedErr.
   373  //
   374  // It only returns an error if there are problems reading the file,
   375  // not for syntax errors in the file itself.
   376  func readGoInfo(f io.Reader, info *fileInfo) error {
   377  	r := newImportReader(info.name, f)
   378  
   379  	r.readKeyword("package")
   380  	r.readIdent()
   381  	for r.peekByte(true) == 'i' {
   382  		r.readKeyword("import")
   383  		if r.peekByte(true) == '(' {
   384  			r.nextByte(false)
   385  			for r.peekByte(true) != ')' && r.err == nil {
   386  				r.readImport()
   387  			}
   388  			r.nextByte(false)
   389  		} else {
   390  			r.readImport()
   391  		}
   392  	}
   393  
   394  	info.header = r.buf
   395  
   396  	// If we stopped successfully before EOF, we read a byte that told us we were done.
   397  	// Return all but that last byte, which would cause a syntax error if we let it through.
   398  	if r.err == nil && !r.eof {
   399  		info.header = r.buf[:len(r.buf)-1]
   400  	}
   401  
   402  	// If we stopped for a syntax error, consume the whole file so that
   403  	// we are sure we don't change the errors that go/parser returns.
   404  	if r.err == errSyntax {
   405  		r.err = nil
   406  		for r.err == nil && !r.eof {
   407  			r.readByte()
   408  		}
   409  		info.header = r.buf
   410  	}
   411  	if r.err != nil {
   412  		return r.err
   413  	}
   414  
   415  	if info.fset == nil {
   416  		return nil
   417  	}
   418  
   419  	// Parse file header & record imports.
   420  	info.parsed, info.parseErr = parser.ParseFile(info.fset, info.name, info.header, parser.ImportsOnly|parser.ParseComments)
   421  	if info.parseErr != nil {
   422  		return nil
   423  	}
   424  
   425  	hasEmbed := false
   426  	for _, decl := range info.parsed.Decls {
   427  		d, ok := decl.(*ast.GenDecl)
   428  		if !ok {
   429  			continue
   430  		}
   431  		for _, dspec := range d.Specs {
   432  			spec, ok := dspec.(*ast.ImportSpec)
   433  			if !ok {
   434  				continue
   435  			}
   436  			quoted := spec.Path.Value
   437  			path, err := strconv.Unquote(quoted)
   438  			if err != nil {
   439  				return fmt.Errorf("parser returned invalid quoted string: <%s>", quoted)
   440  			}
   441  			if path == "embed" {
   442  				hasEmbed = true
   443  			}
   444  
   445  			doc := spec.Doc
   446  			if doc == nil && len(d.Specs) == 1 {
   447  				doc = d.Doc
   448  			}
   449  			info.imports = append(info.imports, fileImport{path, spec.Pos(), doc})
   450  		}
   451  	}
   452  
   453  	// If the file imports "embed",
   454  	// we have to look for //go:embed comments
   455  	// in the remainder of the file.
   456  	// The compiler will enforce the mapping of comments to
   457  	// declared variables. We just need to know the patterns.
   458  	// If there were //go:embed comments earlier in the file
   459  	// (near the package statement or imports), the compiler
   460  	// will reject them. They can be (and have already been) ignored.
   461  	if hasEmbed {
   462  		var line []byte
   463  		for first := true; r.findEmbed(first); first = false {
   464  			line = line[:0]
   465  			pos := r.pos
   466  			for {
   467  				c := r.readByteNoBuf()
   468  				if c == '\n' || r.err != nil || r.eof {
   469  					break
   470  				}
   471  				line = append(line, c)
   472  			}
   473  			// Add args if line is well-formed.
   474  			// Ignore badly-formed lines - the compiler will report them when it finds them,
   475  			// and we can pretend they are not there to help go list succeed with what it knows.
   476  			embs, err := parseGoEmbed(string(line), pos)
   477  			if err == nil {
   478  				info.embeds = append(info.embeds, embs...)
   479  			}
   480  		}
   481  	}
   482  
   483  	return nil
   484  }
   485  
   486  // parseGoEmbed parses the text following "//go:embed" to extract the glob patterns.
   487  // It accepts unquoted space-separated patterns as well as double-quoted and back-quoted Go strings.
   488  // This is based on a similar function in cmd/compile/internal/gc/noder.go;
   489  // this version calculates position information as well.
   490  func parseGoEmbed(args string, pos token.Position) ([]fileEmbed, error) {
   491  	trimBytes := func(n int) {
   492  		pos.Offset += n
   493  		pos.Column += utf8.RuneCountInString(args[:n])
   494  		args = args[n:]
   495  	}
   496  	trimSpace := func() {
   497  		trim := strings.TrimLeftFunc(args, unicode.IsSpace)
   498  		trimBytes(len(args) - len(trim))
   499  	}
   500  
   501  	var list []fileEmbed
   502  	for trimSpace(); args != ""; trimSpace() {
   503  		var path string
   504  		pathPos := pos
   505  	Switch:
   506  		switch args[0] {
   507  		default:
   508  			i := len(args)
   509  			for j, c := range args {
   510  				if unicode.IsSpace(c) {
   511  					i = j
   512  					break
   513  				}
   514  			}
   515  			path = args[:i]
   516  			trimBytes(i)
   517  
   518  		case '`':
   519  			i := strings.Index(args[1:], "`")
   520  			if i < 0 {
   521  				return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args)
   522  			}
   523  			path = args[1 : 1+i]
   524  			trimBytes(1 + i + 1)
   525  
   526  		case '"':
   527  			i := 1
   528  			for ; i < len(args); i++ {
   529  				if args[i] == '\\' {
   530  					i++
   531  					continue
   532  				}
   533  				if args[i] == '"' {
   534  					q, err := strconv.Unquote(args[:i+1])
   535  					if err != nil {
   536  						return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args[:i+1])
   537  					}
   538  					path = q
   539  					trimBytes(i + 1)
   540  					break Switch
   541  				}
   542  			}
   543  			if i >= len(args) {
   544  				return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args)
   545  			}
   546  		}
   547  
   548  		if args != "" {
   549  			r, _ := utf8.DecodeRuneInString(args)
   550  			if !unicode.IsSpace(r) {
   551  				return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args)
   552  			}
   553  		}
   554  		list = append(list, fileEmbed{path, pathPos})
   555  	}
   556  	return list, nil
   557  }
   558  

View as plain text