Black Lives Matter. Support the Equal Justice Initiative.

Source file src/go/build/read.go

Documentation: go/build

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

View as plain text