"fmt"
"go/ast"
"go/doc"
- "go/parser"
"go/token"
"internal/goroot"
"internal/goversion"
p.InvalidGoFiles = append(p.InvalidGoFiles, name)
}
- match, data, filename, err := ctxt.matchFile(p.Dir, name, allTags, &p.BinaryOnly)
+ info, err := ctxt.matchFile(p.Dir, name, allTags, &p.BinaryOnly, fset)
if err != nil {
badFile(err)
continue
}
- if !match {
+ if info == nil {
if strings.HasPrefix(name, "_") || strings.HasPrefix(name, ".") {
// not due to build constraints - don't report
} else if ext == ".go" {
}
continue
}
+ data, filename := info.header, info.name
// Going to save the file. For non-Go files, can stop here.
switch ext {
continue
}
- pf, err := parser.ParseFile(fset, filename, data, parser.ImportsOnly|parser.ParseComments)
- if err != nil {
- badFile(err)
+ if info.parseErr != nil {
+ badFile(info.parseErr)
continue
}
+ pf := info.parsed
pkg := pf.Name.Name
if pkg == "documentation" {
}
// Record imports and information about cgo.
- type importPos struct {
- path string
- pos token.Pos
- }
- var fileImports []importPos
isCgo := false
- for _, decl := range pf.Decls {
- d, ok := decl.(*ast.GenDecl)
- if !ok {
- continue
- }
- for _, dspec := range d.Specs {
- spec, ok := dspec.(*ast.ImportSpec)
- if !ok {
+ for _, imp := range info.imports {
+ if imp.path == "C" {
+ if isTest {
+ badFile(fmt.Errorf("use of cgo in test %s not supported", filename))
continue
}
- quoted := spec.Path.Value
- path, err := strconv.Unquote(quoted)
- if err != nil {
- panic(fmt.Sprintf("%s: parser returned invalid quoted string: <%s>", filename, quoted))
- }
- fileImports = append(fileImports, importPos{path, spec.Pos()})
- if path == "C" {
- if isTest {
- badFile(fmt.Errorf("use of cgo in test %s not supported", filename))
- } else {
- cg := spec.Doc
- if cg == nil && len(d.Specs) == 1 {
- cg = d.Doc
- }
- if cg != nil {
- if err := ctxt.saveCgo(filename, p, cg); err != nil {
- badFile(err)
- }
- }
- isCgo = true
+ isCgo = true
+ if imp.doc != nil {
+ if err := ctxt.saveCgo(filename, p, imp.doc); err != nil {
+ badFile(err)
}
}
}
}
*fileList = append(*fileList, name)
if importMap != nil {
- for _, imp := range fileImports {
+ for _, imp := range info.imports {
importMap[imp.path] = append(importMap[imp.path], fset.Position(imp.pos))
}
}
// MatchFile considers the name of the file and may use ctxt.OpenFile to
// read some or all of the file's content.
func (ctxt *Context) MatchFile(dir, name string) (match bool, err error) {
- match, _, _, err = ctxt.matchFile(dir, name, nil, nil)
- return
+ info, err := ctxt.matchFile(dir, name, nil, nil, nil)
+ return info != nil, err
}
var dummyPkg Package
+// fileInfo records information learned about a file included in a build.
+type fileInfo struct {
+ name string // full name including dir
+ header []byte
+ fset *token.FileSet
+ parsed *ast.File
+ parseErr error
+ imports []fileImport
+}
+
+type fileImport struct {
+ path string
+ pos token.Pos
+ doc *ast.CommentGroup
+}
+
// matchFile determines whether the file with the given name in the given directory
// should be included in the package being constructed.
-// It returns the data read from the file.
+// If the file should be included, matchFile returns a non-nil *fileInfo (and a nil error).
+// Non-nil errors are reserved for unexpected problems.
+//
// If name denotes a Go program, matchFile reads until the end of the
-// imports (and returns that data) even though it only considers text
-// until the first non-comment.
+// imports and returns that section of the file in the fileInfo's header field,
+// even though it only considers text until the first non-comment
+// for +build lines.
+//
// If allTags is non-nil, matchFile records any encountered build tag
// by setting allTags[tag] = true.
-func (ctxt *Context) matchFile(dir, name string, allTags map[string]bool, binaryOnly *bool) (match bool, data []byte, filename string, err error) {
+func (ctxt *Context) matchFile(dir, name string, allTags map[string]bool, binaryOnly *bool, fset *token.FileSet) (*fileInfo, error) {
if strings.HasPrefix(name, "_") ||
strings.HasPrefix(name, ".") {
- return
+ return nil, nil
}
i := strings.LastIndex(name, ".")
ext := name[i:]
if !ctxt.goodOSArchFile(name, allTags) && !ctxt.UseAllFiles {
- return
+ return nil, nil
}
if ext != ".go" && fileListForExt(&dummyPkg, ext) == nil {
// skip
- return
+ return nil, nil
}
+ info := &fileInfo{name: ctxt.joinPath(dir, name), fset: fset}
if ext == ".syso" {
// binary, no reading
- match = true
- return
+ return info, nil
}
- filename = ctxt.joinPath(dir, name)
- f, err := ctxt.openFile(filename)
+ f, err := ctxt.openFile(info.name)
if err != nil {
- return
+ return nil, err
}
- if strings.HasSuffix(filename, ".go") {
- data, err = readImports(f, false, nil)
- if strings.HasSuffix(filename, "_test.go") {
+ if strings.HasSuffix(name, ".go") {
+ err = readGoInfo(f, info)
+ if strings.HasSuffix(name, "_test.go") {
binaryOnly = nil // ignore //go:binary-only-package comments in _test.go files
}
} else {
binaryOnly = nil // ignore //go:binary-only-package comments in non-Go sources
- data, err = readComments(f)
+ info.header, err = readComments(f)
}
f.Close()
if err != nil {
- err = fmt.Errorf("read %s: %v", filename, err)
- return
+ return nil, fmt.Errorf("read %s: %v", info.name, err)
}
// Look for +build comments to accept or reject the file.
- ok, sawBinaryOnly, err := ctxt.shouldBuild(data, allTags)
+ ok, sawBinaryOnly, err := ctxt.shouldBuild(info.header, allTags)
if err != nil {
- return // non-nil err
+ return nil, err
}
if !ok && !ctxt.UseAllFiles {
- return // nil err
+ return nil, nil
}
if binaryOnly != nil && sawBinaryOnly {
*binaryOnly = true
}
- match = true
- return
+
+ return info, nil
}
func cleanImports(m map[string][]token.Position) ([]string, map[string][]token.Position) {
import (
"bufio"
"errors"
+ "fmt"
+ "go/ast"
+ "go/parser"
"io"
+ "strconv"
"unicode/utf8"
)
// readString reads a quoted string literal from the input.
// If an identifier is not present, readString records a syntax error.
-func (r *importReader) readString(save *[]string) {
+func (r *importReader) readString() {
switch r.nextByte(true) {
case '`':
- start := len(r.buf) - 1
for r.err == nil {
if r.nextByte(false) == '`' {
- if save != nil {
- *save = append(*save, string(r.buf[start:]))
- }
break
}
if r.eof {
}
}
case '"':
- start := len(r.buf) - 1
for r.err == nil {
c := r.nextByte(false)
if c == '"' {
- if save != nil {
- *save = append(*save, string(r.buf[start:]))
- }
break
}
if r.eof || c == '\n' {
// readImport reads an import clause - optional identifier followed by quoted string -
// from the input.
-func (r *importReader) readImport(imports *[]string) {
+func (r *importReader) readImport() {
c := r.peekByte(true)
if c == '.' {
r.peek = 0
} else if isIdent(c) {
r.readIdent()
}
- r.readString(imports)
+ r.readString()
}
-// readComments is like ioutil.ReadAll, except that it only reads the leading
+// readComments is like io.ReadAll, except that it only reads the leading
// block of comments in the file.
func readComments(f io.Reader) ([]byte, error) {
r := &importReader{b: bufio.NewReader(f)}
return r.buf, r.err
}
-// readImports is like ioutil.ReadAll, except that it expects a Go file as input
-// and stops reading the input once the imports have completed.
-func readImports(f io.Reader, reportSyntaxError bool, imports *[]string) ([]byte, error) {
+// readGoInfo expects a Go file as input and reads the file up to and including the import section.
+// It records what it learned in *info.
+// If info.fset is non-nil, readGoInfo parses the file and sets info.parsed, info.parseErr,
+// and info.imports.
+//
+// It only returns an error if there are problems reading the file,
+// not for syntax errors in the file itself.
+func readGoInfo(f io.Reader, info *fileInfo) error {
r := &importReader{b: bufio.NewReader(f)}
r.readKeyword("package")
if r.peekByte(true) == '(' {
r.nextByte(false)
for r.peekByte(true) != ')' && r.err == nil {
- r.readImport(imports)
+ r.readImport()
}
r.nextByte(false)
} else {
- r.readImport(imports)
+ r.readImport()
}
}
+ info.header = r.buf
+
// If we stopped successfully before EOF, we read a byte that told us we were done.
// Return all but that last byte, which would cause a syntax error if we let it through.
if r.err == nil && !r.eof {
- return r.buf[:len(r.buf)-1], nil
+ info.header = r.buf[:len(r.buf)-1]
}
// If we stopped for a syntax error, consume the whole file so that
// we are sure we don't change the errors that go/parser returns.
- if r.err == errSyntax && !reportSyntaxError {
+ if r.err == errSyntax {
r.err = nil
for r.err == nil && !r.eof {
r.readByte()
}
+ info.header = r.buf
+ }
+ if r.err != nil {
+ return r.err
}
- return r.buf, r.err
+ if info.fset == nil {
+ return nil
+ }
+
+ // Parse file header & record imports.
+ info.parsed, info.parseErr = parser.ParseFile(info.fset, info.name, info.header, parser.ImportsOnly|parser.ParseComments)
+ if info.parseErr != nil {
+ return nil
+ }
+
+ for _, decl := range info.parsed.Decls {
+ d, ok := decl.(*ast.GenDecl)
+ if !ok {
+ continue
+ }
+ for _, dspec := range d.Specs {
+ spec, ok := dspec.(*ast.ImportSpec)
+ if !ok {
+ continue
+ }
+ quoted := spec.Path.Value
+ path, err := strconv.Unquote(quoted)
+ if err != nil {
+ return fmt.Errorf("parser returned invalid quoted string: <%s>", quoted)
+ }
+
+ doc := spec.Doc
+ if doc == nil && len(d.Specs) == 1 {
+ doc = d.Doc
+ }
+ info.imports = append(info.imports, fileImport{path, spec.Pos(), doc})
+ }
+ }
+
+ return nil
}
const quote = "`"
type readTest struct {
- // Test input contains ℙ where readImports should stop.
+ // Test input contains ℙ where readGoInfo should stop.
in string
err string
}
-var readImportsTests = []readTest{
+var readGoInfoTests = []readTest{
{
`package p`,
"",
},
{
`package p
-
+
// comment
-
+
import "x"
import _ "x"
import a "x"
-
+
/* comment */
-
+
import (
"x" /* comment */
_ "x"
import ()
import()import()import()
import();import();import()
-
+
ℙvar x = 1
`,
"",
/* bar */
/* quux */ // baz
-
+
/*/ zot */
// asdf
}
}
-func TestReadImports(t *testing.T) {
- testRead(t, readImportsTests, func(r io.Reader) ([]byte, error) { return readImports(r, true, nil) })
+func TestReadGoInfo(t *testing.T) {
+ testRead(t, readGoInfoTests, func(r io.Reader) ([]byte, error) {
+ var info fileInfo
+ err := readGoInfo(r, &info)
+ return info.header, err
+ })
}
func TestReadComments(t *testing.T) {
},
}
-func TestReadFailures(t *testing.T) {
- // Errors should be reported (true arg to readImports).
- testRead(t, readFailuresTests, func(r io.Reader) ([]byte, error) { return readImports(r, true, nil) })
-}
-
func TestReadFailuresIgnored(t *testing.T) {
// Syntax errors should not be reported (false arg to readImports).
// Instead, entire file should be the output and no error.
tt.err = ""
}
}
- testRead(t, tests, func(r io.Reader) ([]byte, error) { return readImports(r, false, nil) })
+ testRead(t, tests, func(r io.Reader) ([]byte, error) {
+ var info fileInfo
+ err := readGoInfo(r, &info)
+ return info.header, err
+ })
}