| // Copyright 2018 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package cache |
| |
| import ( |
| "context" |
| "fmt" |
| "go/ast" |
| "go/scanner" |
| "go/token" |
| "go/types" |
| "io/ioutil" |
| "log" |
| "os" |
| "path/filepath" |
| "strings" |
| "sync" |
| |
| "golang.org/x/tools/go/packages" |
| "golang.org/x/tools/internal/lsp/source" |
| ) |
| |
| type View struct { |
| mu sync.Mutex // protects all mutable state of the view |
| |
| Config packages.Config |
| |
| files map[source.URI]*File |
| |
| analysisCache *source.AnalysisCache |
| } |
| |
| // NewView creates a new View, given a root path and go/packages configuration. |
| // If config is nil, one is created with the directory set to the rootPath. |
| func NewView(config *packages.Config) *View { |
| return &View{ |
| Config: *config, |
| files: make(map[source.URI]*File), |
| } |
| } |
| |
| func (v *View) FileSet() *token.FileSet { |
| return v.Config.Fset |
| } |
| |
| func (v *View) GetAnalysisCache() *source.AnalysisCache { |
| if v.analysisCache == nil { |
| v.analysisCache = source.NewAnalysisCache() |
| } |
| return v.analysisCache |
| } |
| |
| // SetContent sets the overlay contents for a file. A nil content value will |
| // remove the file from the active set and revert it to its on-disk contents. |
| func (v *View) SetContent(ctx context.Context, uri source.URI, content []byte) (source.View, error) { |
| v.mu.Lock() |
| defer v.mu.Unlock() |
| |
| f := v.getFile(uri) |
| f.content = content |
| |
| // Resetting the contents invalidates the ast, token, and pkg fields. |
| f.ast = nil |
| f.token = nil |
| f.pkg = nil |
| |
| // We might need to update the overlay. |
| switch { |
| case f.active && content == nil: |
| // The file was active, so we need to forget its content. |
| f.active = false |
| if filename, err := f.URI.Filename(); err == nil { |
| delete(f.view.Config.Overlay, filename) |
| } |
| f.content = nil |
| case content != nil: |
| // This is an active overlay, so we update the map. |
| f.active = true |
| if filename, err := f.URI.Filename(); err == nil { |
| f.view.Config.Overlay[filename] = f.content |
| } |
| } |
| |
| // TODO(rstambler): We should really return a new, updated view. |
| return v, nil |
| } |
| |
| // GetFile returns a File for the given URI. It will always succeed because it |
| // adds the file to the managed set if needed. |
| func (v *View) GetFile(ctx context.Context, uri source.URI) (source.File, error) { |
| v.mu.Lock() |
| f := v.getFile(uri) |
| v.mu.Unlock() |
| return f, nil |
| } |
| |
| // getFile is the unlocked internal implementation of GetFile. |
| func (v *View) getFile(uri source.URI) *File { |
| f, found := v.files[uri] |
| if !found { |
| f = &File{ |
| URI: uri, |
| view: v, |
| } |
| v.files[uri] = f |
| } |
| return f |
| } |
| |
| func (v *View) parse(uri source.URI) error { |
| path, err := uri.Filename() |
| if err != nil { |
| return err |
| } |
| pkgs, err := packages.Load(&v.Config, fmt.Sprintf("file=%s", path)) |
| if len(pkgs) == 0 { |
| if err == nil { |
| err = fmt.Errorf("no packages found for %s", path) |
| } |
| return err |
| } |
| var foundPkg bool // true if we found the package for uri |
| for _, pkg := range pkgs { |
| imp := &importer{ |
| entries: make(map[string]*entry), |
| packages: make(map[string]*packages.Package), |
| v: v, |
| topLevelPkgPath: pkg.PkgPath, |
| } |
| |
| // TODO(rstambler): Get real TypeSizes from go/packages. |
| pkg.TypesSizes = &types.StdSizes{} |
| |
| if err := imp.addImports(pkg); err != nil { |
| return err |
| } |
| imp.importPackage(pkg.PkgPath) |
| |
| // Add every file in this package to our cache. |
| for _, file := range pkg.Syntax { |
| // TODO: If a file is in multiple packages, which package do we store? |
| if !file.Pos().IsValid() { |
| log.Printf("invalid position for file %v", file.Name) |
| continue |
| } |
| tok := imp.v.Config.Fset.File(file.Pos()) |
| if tok == nil { |
| log.Printf("no token.File for %v", file.Name) |
| continue |
| } |
| fURI := source.ToURI(tok.Name()) |
| if fURI == uri { |
| foundPkg = true |
| } |
| f := imp.v.getFile(fURI) |
| f.token = tok |
| f.ast = file |
| f.pkg = pkg |
| } |
| } |
| if !foundPkg { |
| return fmt.Errorf("no package found for %v", uri) |
| } |
| return nil |
| } |
| |
| type importer struct { |
| mu sync.Mutex |
| entries map[string]*entry |
| packages map[string]*packages.Package |
| topLevelPkgPath string |
| |
| v *View |
| } |
| |
| type entry struct { |
| pkg *types.Package |
| err error |
| ready chan struct{} |
| } |
| |
| func (imp *importer) addImports(pkg *packages.Package) error { |
| imp.packages[pkg.PkgPath] = pkg |
| for _, i := range pkg.Imports { |
| if i.PkgPath == pkg.PkgPath { |
| return fmt.Errorf("import cycle: [%v]", pkg.PkgPath) |
| } |
| if err := imp.addImports(i); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| func (imp *importer) Import(path string) (*types.Package, error) { |
| if path == imp.topLevelPkgPath { |
| return nil, fmt.Errorf("import cycle: [%v]", path) |
| } |
| imp.mu.Lock() |
| e, ok := imp.entries[path] |
| if ok { |
| // cache hit |
| imp.mu.Unlock() |
| // wait for entry to become ready |
| <-e.ready |
| } else { |
| // cache miss |
| e = &entry{ready: make(chan struct{})} |
| imp.entries[path] = e |
| imp.mu.Unlock() |
| |
| // This goroutine becomes responsible for populating |
| // the entry and broadcasting its readiness. |
| e.pkg, e.err = imp.importPackage(path) |
| close(e.ready) |
| } |
| return e.pkg, e.err |
| } |
| |
| func (imp *importer) importPackage(pkgPath string) (*types.Package, error) { |
| imp.mu.Lock() |
| pkg, ok := imp.packages[pkgPath] |
| imp.mu.Unlock() |
| if !ok { |
| return nil, fmt.Errorf("no metadata for %v", pkgPath) |
| } |
| pkg.Fset = imp.v.Config.Fset |
| appendError := func(err error) { |
| imp.appendPkgError(pkg, err) |
| } |
| files, errs := imp.parseFiles(pkg.CompiledGoFiles) |
| for _, err := range errs { |
| appendError(err) |
| } |
| pkg.Syntax = files |
| cfg := &types.Config{ |
| Error: appendError, |
| Importer: imp, |
| } |
| pkg.Types = types.NewPackage(pkg.PkgPath, pkg.Name) |
| pkg.TypesInfo = &types.Info{ |
| Types: make(map[ast.Expr]types.TypeAndValue), |
| Defs: make(map[*ast.Ident]types.Object), |
| Uses: make(map[*ast.Ident]types.Object), |
| Implicits: make(map[ast.Node]types.Object), |
| Selections: make(map[*ast.SelectorExpr]*types.Selection), |
| Scopes: make(map[ast.Node]*types.Scope), |
| } |
| check := types.NewChecker(cfg, imp.v.Config.Fset, pkg.Types, pkg.TypesInfo) |
| check.Files(pkg.Syntax) |
| |
| return pkg.Types, nil |
| } |
| |
| func (imp *importer) appendPkgError(pkg *packages.Package, err error) { |
| if err == nil { |
| return |
| } |
| var errs []packages.Error |
| switch err := err.(type) { |
| case *scanner.Error: |
| errs = append(errs, packages.Error{ |
| Pos: err.Pos.String(), |
| Msg: err.Msg, |
| Kind: packages.ParseError, |
| }) |
| case scanner.ErrorList: |
| // The first parser error is likely the root cause of the problem. |
| if err.Len() > 0 { |
| errs = append(errs, packages.Error{ |
| Pos: err[0].Pos.String(), |
| Msg: err[0].Msg, |
| Kind: packages.ParseError, |
| }) |
| } |
| case types.Error: |
| errs = append(errs, packages.Error{ |
| Pos: imp.v.Config.Fset.Position(err.Pos).String(), |
| Msg: err.Msg, |
| Kind: packages.TypeError, |
| }) |
| } |
| pkg.Errors = append(pkg.Errors, errs...) |
| } |
| |
| // We use a counting semaphore to limit |
| // the number of parallel I/O calls per process. |
| var ioLimit = make(chan bool, 20) |
| |
| // parseFiles reads and parses the Go source files and returns the ASTs |
| // of the ones that could be at least partially parsed, along with a |
| // list of I/O and parse errors encountered. |
| // |
| // Because files are scanned in parallel, the token.Pos |
| // positions of the resulting ast.Files are not ordered. |
| // |
| func (imp *importer) parseFiles(filenames []string) ([]*ast.File, []error) { |
| var wg sync.WaitGroup |
| n := len(filenames) |
| parsed := make([]*ast.File, n) |
| errors := make([]error, n) |
| for i, file := range filenames { |
| if imp.v.Config.Context != nil { |
| if imp.v.Config.Context.Err() != nil { |
| parsed[i] = nil |
| errors[i] = imp.v.Config.Context.Err() |
| continue |
| } |
| } |
| wg.Add(1) |
| go func(i int, filename string) { |
| ioLimit <- true // wait |
| // ParseFile may return both an AST and an error. |
| var src []byte |
| for f, contents := range imp.v.Config.Overlay { |
| if sameFile(f, filename) { |
| src = contents |
| } |
| } |
| var err error |
| if src == nil { |
| src, err = ioutil.ReadFile(filename) |
| } |
| if err != nil { |
| parsed[i], errors[i] = nil, err |
| } else { |
| parsed[i], errors[i] = imp.v.Config.ParseFile(imp.v.Config.Fset, filename, src) |
| } |
| <-ioLimit // signal |
| wg.Done() |
| }(i, file) |
| } |
| wg.Wait() |
| |
| // Eliminate nils, preserving order. |
| var o int |
| for _, f := range parsed { |
| if f != nil { |
| parsed[o] = f |
| o++ |
| } |
| } |
| parsed = parsed[:o] |
| |
| o = 0 |
| for _, err := range errors { |
| if err != nil { |
| errors[o] = err |
| o++ |
| } |
| } |
| errors = errors[:o] |
| |
| return parsed, errors |
| } |
| |
| // sameFile returns true if x and y have the same basename and denote |
| // the same file. |
| // |
| func sameFile(x, y string) bool { |
| if x == y { |
| // It could be the case that y doesn't exist. |
| // For instance, it may be an overlay file that |
| // hasn't been written to disk. To handle that case |
| // let x == y through. (We added the exact absolute path |
| // string to the CompiledGoFiles list, so the unwritten |
| // overlay case implies x==y.) |
| return true |
| } |
| if strings.EqualFold(filepath.Base(x), filepath.Base(y)) { // (optimisation) |
| if xi, err := os.Stat(x); err == nil { |
| if yi, err := os.Stat(y); err == nil { |
| return os.SameFile(xi, yi) |
| } |
| } |
| } |
| return false |
| } |