Home | History | Annotate | Download | only in srcimporter
      1 // Copyright 2017 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 srcimporter implements importing directly
      6 // from source files rather than installed packages.
      7 package srcimporter // import "go/internal/srcimporter"
      8 
      9 import (
     10 	"fmt"
     11 	"go/ast"
     12 	"go/build"
     13 	"go/parser"
     14 	"go/token"
     15 	"go/types"
     16 	"path/filepath"
     17 	"sync"
     18 )
     19 
     20 // An Importer provides the context for importing packages from source code.
     21 type Importer struct {
     22 	ctxt     *build.Context
     23 	fset     *token.FileSet
     24 	sizes    types.Sizes
     25 	packages map[string]*types.Package
     26 }
     27 
     28 // NewImporter returns a new Importer for the given context, file set, and map
     29 // of packages. The context is used to resolve import paths to package paths,
     30 // and identifying the files belonging to the package. If the context provides
     31 // non-nil file system functions, they are used instead of the regular package
     32 // os functions. The file set is used to track position information of package
     33 // files; and imported packages are added to the packages map.
     34 func New(ctxt *build.Context, fset *token.FileSet, packages map[string]*types.Package) *Importer {
     35 	return &Importer{
     36 		ctxt:     ctxt,
     37 		fset:     fset,
     38 		sizes:    types.SizesFor(ctxt.Compiler, ctxt.GOARCH), // uses go/types default if GOARCH not found
     39 		packages: packages,
     40 	}
     41 }
     42 
     43 // Importing is a sentinel taking the place in Importer.packages
     44 // for a package that is in the process of being imported.
     45 var importing types.Package
     46 
     47 // Import(path) is a shortcut for ImportFrom(path, "", 0).
     48 func (p *Importer) Import(path string) (*types.Package, error) {
     49 	return p.ImportFrom(path, "", 0)
     50 }
     51 
     52 // ImportFrom imports the package with the given import path resolved from the given srcDir,
     53 // adds the new package to the set of packages maintained by the importer, and returns the
     54 // package. Package path resolution and file system operations are controlled by the context
     55 // maintained with the importer. The import mode must be zero but is otherwise ignored.
     56 // Packages that are not comprised entirely of pure Go files may fail to import because the
     57 // type checker may not be able to determine all exported entities (e.g. due to cgo dependencies).
     58 func (p *Importer) ImportFrom(path, srcDir string, mode types.ImportMode) (*types.Package, error) {
     59 	if mode != 0 {
     60 		panic("non-zero import mode")
     61 	}
     62 
     63 	// determine package path (do vendor resolution)
     64 	var bp *build.Package
     65 	var err error
     66 	switch {
     67 	default:
     68 		if abs, err := p.absPath(srcDir); err == nil { // see issue #14282
     69 			srcDir = abs
     70 		}
     71 		bp, err = p.ctxt.Import(path, srcDir, build.FindOnly)
     72 
     73 	case build.IsLocalImport(path):
     74 		// "./x" -> "srcDir/x"
     75 		bp, err = p.ctxt.ImportDir(filepath.Join(srcDir, path), build.FindOnly)
     76 
     77 	case p.isAbsPath(path):
     78 		return nil, fmt.Errorf("invalid absolute import path %q", path)
     79 	}
     80 	if err != nil {
     81 		return nil, err // err may be *build.NoGoError - return as is
     82 	}
     83 
     84 	// package unsafe is known to the type checker
     85 	if bp.ImportPath == "unsafe" {
     86 		return types.Unsafe, nil
     87 	}
     88 
     89 	// no need to re-import if the package was imported completely before
     90 	pkg := p.packages[bp.ImportPath]
     91 	if pkg != nil {
     92 		if pkg == &importing {
     93 			return nil, fmt.Errorf("import cycle through package %q", bp.ImportPath)
     94 		}
     95 		if !pkg.Complete() {
     96 			// Package exists but is not complete - we cannot handle this
     97 			// at the moment since the source importer replaces the package
     98 			// wholesale rather than augmenting it (see #19337 for details).
     99 			// Return incomplete package with error (see #16088).
    100 			return pkg, fmt.Errorf("reimported partially imported package %q", bp.ImportPath)
    101 		}
    102 		return pkg, nil
    103 	}
    104 
    105 	p.packages[bp.ImportPath] = &importing
    106 	defer func() {
    107 		// clean up in case of error
    108 		// TODO(gri) Eventually we may want to leave a (possibly empty)
    109 		// package in the map in all cases (and use that package to
    110 		// identify cycles). See also issue 16088.
    111 		if p.packages[bp.ImportPath] == &importing {
    112 			p.packages[bp.ImportPath] = nil
    113 		}
    114 	}()
    115 
    116 	// collect package files
    117 	bp, err = p.ctxt.ImportDir(bp.Dir, 0)
    118 	if err != nil {
    119 		return nil, err // err may be *build.NoGoError - return as is
    120 	}
    121 	var filenames []string
    122 	filenames = append(filenames, bp.GoFiles...)
    123 	filenames = append(filenames, bp.CgoFiles...)
    124 
    125 	files, err := p.parseFiles(bp.Dir, filenames)
    126 	if err != nil {
    127 		return nil, err
    128 	}
    129 
    130 	// type-check package files
    131 	var firstHardErr error
    132 	conf := types.Config{
    133 		IgnoreFuncBodies: true,
    134 		FakeImportC:      true,
    135 		// continue type-checking after the first error
    136 		Error: func(err error) {
    137 			if firstHardErr == nil && !err.(types.Error).Soft {
    138 				firstHardErr = err
    139 			}
    140 		},
    141 		Importer: p,
    142 		Sizes:    p.sizes,
    143 	}
    144 	pkg, err = conf.Check(bp.ImportPath, p.fset, files, nil)
    145 	if err != nil {
    146 		// If there was a hard error it is possibly unsafe
    147 		// to use the package as it may not be fully populated.
    148 		// Do not return it (see also #20837, #20855).
    149 		if firstHardErr != nil {
    150 			pkg = nil
    151 			err = firstHardErr // give preference to first hard error over any soft error
    152 		}
    153 		return pkg, fmt.Errorf("type-checking package %q failed (%v)", bp.ImportPath, err)
    154 	}
    155 	if firstHardErr != nil {
    156 		// this can only happen if we have a bug in go/types
    157 		panic("package is not safe yet no error was returned")
    158 	}
    159 
    160 	p.packages[bp.ImportPath] = pkg
    161 	return pkg, nil
    162 }
    163 
    164 func (p *Importer) parseFiles(dir string, filenames []string) ([]*ast.File, error) {
    165 	open := p.ctxt.OpenFile // possibly nil
    166 
    167 	files := make([]*ast.File, len(filenames))
    168 	errors := make([]error, len(filenames))
    169 
    170 	var wg sync.WaitGroup
    171 	wg.Add(len(filenames))
    172 	for i, filename := range filenames {
    173 		go func(i int, filepath string) {
    174 			defer wg.Done()
    175 			if open != nil {
    176 				src, err := open(filepath)
    177 				if err != nil {
    178 					errors[i] = fmt.Errorf("opening package file %s failed (%v)", filepath, err)
    179 					return
    180 				}
    181 				files[i], errors[i] = parser.ParseFile(p.fset, filepath, src, 0)
    182 				src.Close() // ignore Close error - parsing may have succeeded which is all we need
    183 			} else {
    184 				// Special-case when ctxt doesn't provide a custom OpenFile and use the
    185 				// parser's file reading mechanism directly. This appears to be quite a
    186 				// bit faster than opening the file and providing an io.ReaderCloser in
    187 				// both cases.
    188 				// TODO(gri) investigate performance difference (issue #19281)
    189 				files[i], errors[i] = parser.ParseFile(p.fset, filepath, nil, 0)
    190 			}
    191 		}(i, p.joinPath(dir, filename))
    192 	}
    193 	wg.Wait()
    194 
    195 	// if there are errors, return the first one for deterministic results
    196 	for _, err := range errors {
    197 		if err != nil {
    198 			return nil, err
    199 		}
    200 	}
    201 
    202 	return files, nil
    203 }
    204 
    205 // context-controlled file system operations
    206 
    207 func (p *Importer) absPath(path string) (string, error) {
    208 	// TODO(gri) This should be using p.ctxt.AbsPath which doesn't
    209 	// exist but probably should. See also issue #14282.
    210 	return filepath.Abs(path)
    211 }
    212 
    213 func (p *Importer) isAbsPath(path string) bool {
    214 	if f := p.ctxt.IsAbsPath; f != nil {
    215 		return f(path)
    216 	}
    217 	return filepath.IsAbs(path)
    218 }
    219 
    220 func (p *Importer) joinPath(elem ...string) string {
    221 	if f := p.ctxt.JoinPath; f != nil {
    222 		return f(elem...)
    223 	}
    224 	return filepath.Join(elem...)
    225 }
    226