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.
5 // This file is a lightly modified copy go/build/read.go with unused parts
26 type importReader struct {
36 var bom = []byte{0xef, 0xbb, 0xbf}
38 func newImportReader(name string, r io.Reader) *importReader {
39 b := bufio.NewReader(r)
40 // Remove leading UTF-8 BOM.
41 // Per https://golang.org/ref/spec#Source_code_representation:
42 // a compiler may ignore a UTF-8-encoded byte order mark (U+FEFF)
43 // if it is the first Unicode code point in the source text.
44 if leadingBytes, err := b.Peek(3); err == nil && bytes.Equal(leadingBytes, bom) {
57 func isIdent(c byte) bool {
58 return 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9' || c == '_' || c >= utf8.RuneSelf
62 errSyntax = errors.New("syntax error")
63 errNUL = errors.New("unexpected NUL in input")
66 // syntaxError records a syntax error, but only if an I/O error has not already been recorded.
67 func (r *importReader) syntaxError() {
73 // readByte reads the next byte from the input, saves it in buf, and returns it.
74 // If an error occurs, readByte records the error in r.err and returns 0.
75 func (r *importReader) readByte() byte {
76 c, err := r.b.ReadByte()
78 r.buf = append(r.buf, c)
86 } else if r.err == nil {
94 // readByteNoBuf is like readByte but doesn't buffer the byte.
95 // It exhausts r.buf before reading from r.b.
96 func (r *importReader) readByteNoBuf() byte {
103 c, err = r.b.ReadByte()
104 if err == nil && c == 0 {
112 } else if r.err == nil {
127 // peekByte returns the next byte from the input reader but does not advance beyond it.
128 // If skipSpace is set, peekByte skips leading spaces and comments.
129 func (r *importReader) peekByte(skipSpace bool) byte {
131 if r.nerr++; r.nerr > 10000 {
132 panic("go/build: import reader looping")
137 // Use r.peek as first input byte.
138 // Don't just return r.peek here: it might have been left by peekByte(false)
139 // and this might be peekByte(true).
144 for r.err == nil && !r.eof {
146 // For the purposes of this reader, semicolons are never necessary to
147 // understand the input and are treated as spaces.
149 case ' ', '\f', '\t', '\r', '\n', ';':
156 for c != '\n' && r.err == nil && !r.eof {
161 for (c != '*' || c1 != '/') && r.err == nil {
165 c, c1 = c1, r.readByte()
180 // nextByte is like peekByte but advances beyond the returned byte.
181 func (r *importReader) nextByte(skipSpace bool) byte {
182 c := r.peekByte(skipSpace)
187 var goEmbed = []byte("go:embed")
189 // findEmbed advances the input reader to the next //go:embed comment.
190 // It reports whether it found a comment.
191 // (Otherwise it found an error or EOF.)
192 func (r *importReader) findEmbed(first bool) bool {
193 // The import block scan stopped after a non-space character,
194 // so the reader is not at the start of a line on the first call.
195 // After that, each //go:embed extraction leaves the reader
196 // at the end of a line.
199 for r.err == nil && !r.eof {
200 c = r.readByteNoBuf()
210 // leave startLine alone
218 c = r.readByteNoBuf()
228 c = r.readByteNoBuf()
240 c = r.readByteNoBuf()
242 c = r.readByteNoBuf()
253 c = r.readByteNoBuf()
263 c = r.readByteNoBuf()
269 c = r.readByteNoBuf()
277 for (c != '*' || c1 != '/') && r.err == nil {
281 c, c1 = c1, r.readByteNoBuf()
287 // Try to read this as a //go:embed comment.
288 for i := range goEmbed {
289 c = r.readByteNoBuf()
294 c = r.readByteNoBuf()
295 if c == ' ' || c == '\t' {
301 for c != '\n' && r.err == nil && !r.eof {
302 c = r.readByteNoBuf()
311 // readKeyword reads the given keyword from the input.
312 // If the keyword is not present, readKeyword records a syntax error.
313 func (r *importReader) readKeyword(kw string) {
315 for i := 0; i < len(kw); i++ {
316 if r.nextByte(false) != kw[i] {
321 if isIdent(r.peekByte(false)) {
326 // readIdent reads an identifier from the input.
327 // If an identifier is not present, readIdent records a syntax error.
328 func (r *importReader) readIdent() {
329 c := r.peekByte(true)
334 for isIdent(r.peekByte(false)) {
339 // readString reads a quoted string literal from the input.
340 // If an identifier is not present, readString records a syntax error.
341 func (r *importReader) readString() {
342 switch r.nextByte(true) {
345 if r.nextByte(false) == '`' {
354 c := r.nextByte(false)
358 if r.eof || c == '\n' {
370 // readImport reads an import clause - optional identifier followed by quoted string -
372 func (r *importReader) readImport() {
373 c := r.peekByte(true)
376 } else if isIdent(c) {
382 // readComments is like io.ReadAll, except that it only reads the leading
383 // block of comments in the file.
384 func readComments(f io.Reader) ([]byte, error) {
385 r := newImportReader("", f)
387 if r.err == nil && !r.eof {
388 // Didn't reach EOF, so must have found a non-space byte. Remove it.
389 r.buf = r.buf[:len(r.buf)-1]
394 // readGoInfo expects a Go file as input and reads the file up to and including the import section.
395 // It records what it learned in *info.
396 // If info.fset is non-nil, readGoInfo parses the file and sets info.parsed, info.parseErr,
397 // info.imports and info.embeds.
399 // It only returns an error if there are problems reading the file,
400 // not for syntax errors in the file itself.
401 func readGoInfo(f io.Reader, info *fileInfo) error {
402 r := newImportReader(info.name, f)
404 r.readKeyword("package")
406 for r.peekByte(true) == 'i' {
407 r.readKeyword("import")
408 if r.peekByte(true) == '(' {
410 for r.peekByte(true) != ')' && r.err == nil {
421 // If we stopped successfully before EOF, we read a byte that told us we were done.
422 // Return all but that last byte, which would cause a syntax error if we let it through.
423 if r.err == nil && !r.eof {
424 info.header = r.buf[:len(r.buf)-1]
427 // If we stopped for a syntax error, consume the whole file so that
428 // we are sure we don't change the errors that go/parser returns.
429 if r.err == errSyntax {
431 for r.err == nil && !r.eof {
440 if info.fset == nil {
444 // Parse file header & record imports.
445 info.parsed, info.parseErr = parser.ParseFile(info.fset, info.name, info.header, parser.ImportsOnly|parser.ParseComments)
446 if info.parseErr != nil {
451 for _, decl := range info.parsed.Decls {
452 d, ok := decl.(*ast.GenDecl)
456 for _, dspec := range d.Specs {
457 spec, ok := dspec.(*ast.ImportSpec)
461 quoted := spec.Path.Value
462 path, err := strconv.Unquote(quoted)
464 return fmt.Errorf("parser returned invalid quoted string: <%s>", quoted)
471 if doc == nil && len(d.Specs) == 1 {
474 info.imports = append(info.imports, fileImport{path, spec.Pos(), doc})
478 // Extract directives.
479 for _, group := range info.parsed.Comments {
480 if group.Pos() >= info.parsed.Package {
483 for _, c := range group.List {
484 if strings.HasPrefix(c.Text, "//go:") {
485 info.directives = append(info.directives, build.Directive{Text: c.Text, Pos: info.fset.Position(c.Slash)})
490 // If the file imports "embed",
491 // we have to look for //go:embed comments
492 // in the remainder of the file.
493 // The compiler will enforce the mapping of comments to
494 // declared variables. We just need to know the patterns.
495 // If there were //go:embed comments earlier in the file
496 // (near the package statement or imports), the compiler
497 // will reject them. They can be (and have already been) ignored.
500 for first := true; r.findEmbed(first); first = false {
504 c := r.readByteNoBuf()
505 if c == '\n' || r.err != nil || r.eof {
508 line = append(line, c)
510 // Add args if line is well-formed.
511 // Ignore badly-formed lines - the compiler will report them when it finds them,
512 // and we can pretend they are not there to help go list succeed with what it knows.
513 embs, err := parseGoEmbed(string(line), pos)
515 info.embeds = append(info.embeds, embs...)
523 // parseGoEmbed parses the text following "//go:embed" to extract the glob patterns.
524 // It accepts unquoted space-separated patterns as well as double-quoted and back-quoted Go strings.
525 // This is based on a similar function in cmd/compile/internal/gc/noder.go;
526 // this version calculates position information as well.
527 func parseGoEmbed(args string, pos token.Position) ([]fileEmbed, error) {
528 trimBytes := func(n int) {
530 pos.Column += utf8.RuneCountInString(args[:n])
533 trimSpace := func() {
534 trim := strings.TrimLeftFunc(args, unicode.IsSpace)
535 trimBytes(len(args) - len(trim))
539 for trimSpace(); args != ""; trimSpace() {
546 for j, c := range args {
547 if unicode.IsSpace(c) {
557 path, _, ok = strings.Cut(args[1:], "`")
559 return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args)
561 trimBytes(1 + len(path) + 1)
565 for ; i < len(args); i++ {
571 q, err := strconv.Unquote(args[:i+1])
573 return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args[:i+1])
581 return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args)
586 r, _ := utf8.DecodeRuneInString(args)
587 if !unicode.IsSpace(r) {
588 return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args)
591 list = append(list, fileEmbed{path, pathPos})