519 lines
14 KiB
Go
519 lines
14 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"go/ast"
|
|
"go/build"
|
|
"go/parser"
|
|
"go/token"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
//-------------------------------------------------------------------------
|
|
// []package_import
|
|
//-------------------------------------------------------------------------
|
|
|
|
type package_import struct {
|
|
alias string
|
|
path string
|
|
}
|
|
|
|
// Parses import declarations until the first non-import declaration and fills
|
|
// `packages` array with import information.
|
|
func collect_package_imports(filename string, decls []ast.Decl, context *package_lookup_context) []package_import {
|
|
pi := make([]package_import, 0, 16)
|
|
for _, decl := range decls {
|
|
if gd, ok := decl.(*ast.GenDecl); ok && gd.Tok == token.IMPORT {
|
|
for _, spec := range gd.Specs {
|
|
imp := spec.(*ast.ImportSpec)
|
|
path, alias := path_and_alias(imp)
|
|
path, ok := abs_path_for_package(filename, path, context)
|
|
if ok && alias != "_" {
|
|
pi = append(pi, package_import{alias, path})
|
|
}
|
|
}
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
return pi
|
|
}
|
|
|
|
//-------------------------------------------------------------------------
|
|
// decl_file_cache
|
|
//
|
|
// Contains cache for top-level declarations of a file as well as its
|
|
// contents, AST and import information.
|
|
//-------------------------------------------------------------------------
|
|
|
|
type decl_file_cache struct {
|
|
name string // file name
|
|
mtime int64 // last modification time
|
|
|
|
decls map[string]*decl // top-level declarations
|
|
error error // last error
|
|
packages []package_import // import information
|
|
filescope *scope
|
|
|
|
fset *token.FileSet
|
|
context *package_lookup_context
|
|
}
|
|
|
|
func new_decl_file_cache(name string, context *package_lookup_context) *decl_file_cache {
|
|
return &decl_file_cache{
|
|
name: name,
|
|
context: context,
|
|
}
|
|
}
|
|
|
|
func (f *decl_file_cache) update() {
|
|
stat, err := os.Stat(f.name)
|
|
if err != nil {
|
|
f.decls = nil
|
|
f.error = err
|
|
f.fset = nil
|
|
return
|
|
}
|
|
|
|
statmtime := stat.ModTime().UnixNano()
|
|
if f.mtime == statmtime {
|
|
return
|
|
}
|
|
|
|
f.mtime = statmtime
|
|
f.read_file()
|
|
}
|
|
|
|
func (f *decl_file_cache) read_file() {
|
|
var data []byte
|
|
data, f.error = file_reader.read_file(f.name)
|
|
if f.error != nil {
|
|
return
|
|
}
|
|
data, _ = filter_out_shebang(data)
|
|
|
|
f.process_data(data)
|
|
}
|
|
|
|
func (f *decl_file_cache) process_data(data []byte) {
|
|
var file *ast.File
|
|
f.fset = token.NewFileSet()
|
|
file, f.error = parser.ParseFile(f.fset, "", data, 0)
|
|
f.filescope = new_scope(nil)
|
|
for _, d := range file.Decls {
|
|
anonymify_ast(d, 0, f.filescope)
|
|
}
|
|
f.packages = collect_package_imports(f.name, file.Decls, f.context)
|
|
f.decls = make(map[string]*decl, len(file.Decls))
|
|
for _, decl := range file.Decls {
|
|
append_to_top_decls(f.decls, decl, f.filescope)
|
|
}
|
|
}
|
|
|
|
func append_to_top_decls(decls map[string]*decl, decl ast.Decl, scope *scope) {
|
|
foreach_decl(decl, func(data *foreach_decl_struct) {
|
|
class := ast_decl_class(data.decl)
|
|
for i, name := range data.names {
|
|
typ, v, vi := data.type_value_index(i)
|
|
|
|
d := new_decl_full(name.Name, class, 0, typ, v, vi, scope)
|
|
if d == nil {
|
|
return
|
|
}
|
|
|
|
methodof := method_of(decl)
|
|
if methodof != "" {
|
|
decl, ok := decls[methodof]
|
|
if ok {
|
|
decl.add_child(d)
|
|
} else {
|
|
decl = new_decl(methodof, decl_methods_stub, scope)
|
|
decls[methodof] = decl
|
|
decl.add_child(d)
|
|
}
|
|
} else {
|
|
decl, ok := decls[d.name]
|
|
if ok {
|
|
decl.expand_or_replace(d)
|
|
} else {
|
|
decls[d.name] = d
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func abs_path_for_package(filename, p string, context *package_lookup_context) (string, bool) {
|
|
dir, _ := filepath.Split(filename)
|
|
if len(p) == 0 {
|
|
return "", false
|
|
}
|
|
if p[0] == '.' {
|
|
return fmt.Sprintf("%s.a", filepath.Join(dir, p)), true
|
|
}
|
|
pkg, ok := find_go_dag_package(p, dir)
|
|
if ok {
|
|
return pkg, true
|
|
}
|
|
return find_global_file(p, context)
|
|
}
|
|
|
|
func path_and_alias(imp *ast.ImportSpec) (string, string) {
|
|
path := ""
|
|
if imp.Path != nil && len(imp.Path.Value) > 0 {
|
|
path = string(imp.Path.Value)
|
|
path = path[1 : len(path)-1]
|
|
}
|
|
alias := ""
|
|
if imp.Name != nil {
|
|
alias = imp.Name.Name
|
|
}
|
|
return path, alias
|
|
}
|
|
|
|
func find_go_dag_package(imp, filedir string) (string, bool) {
|
|
// Support godag directory structure
|
|
dir, pkg := filepath.Split(imp)
|
|
godag_pkg := filepath.Join(filedir, "..", dir, "_obj", pkg+".a")
|
|
if file_exists(godag_pkg) {
|
|
return godag_pkg, true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// autobuild compares the mod time of the source files of the package, and if any of them is newer
|
|
// than the package object file will rebuild it.
|
|
func autobuild(p *build.Package) error {
|
|
if p.Dir == "" {
|
|
return fmt.Errorf("no files to build")
|
|
}
|
|
ps, err := os.Stat(p.PkgObj)
|
|
if err != nil {
|
|
// Assume package file does not exist and build for the first time.
|
|
return build_package(p)
|
|
}
|
|
pt := ps.ModTime()
|
|
fs, err := readdir_lstat(p.Dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, f := range fs {
|
|
if f.IsDir() {
|
|
continue
|
|
}
|
|
if f.ModTime().After(pt) {
|
|
// Source file is newer than package file; rebuild.
|
|
return build_package(p)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// build_package builds the package by calling `go install package/import`. If everything compiles
|
|
// correctly, the newly compiled package should then be in the usual place in the `$GOPATH/pkg`
|
|
// directory, and gocode will pick it up from there.
|
|
func build_package(p *build.Package) error {
|
|
if *g_debug {
|
|
log.Printf("-------------------")
|
|
log.Printf("rebuilding package %s", p.Name)
|
|
log.Printf("package import: %s", p.ImportPath)
|
|
log.Printf("package object: %s", p.PkgObj)
|
|
log.Printf("package source dir: %s", p.Dir)
|
|
log.Printf("package source files: %v", p.GoFiles)
|
|
log.Printf("GOPATH: %v", g_daemon.context.GOPATH)
|
|
log.Printf("GOROOT: %v", g_daemon.context.GOROOT)
|
|
}
|
|
env := os.Environ()
|
|
for i, v := range env {
|
|
if strings.HasPrefix(v, "GOPATH=") {
|
|
env[i] = "GOPATH=" + g_daemon.context.GOPATH
|
|
} else if strings.HasPrefix(v, "GOROOT=") {
|
|
env[i] = "GOROOT=" + g_daemon.context.GOROOT
|
|
}
|
|
}
|
|
|
|
cmd := exec.Command("go", "install", p.ImportPath)
|
|
cmd.Env = env
|
|
|
|
// TODO: Should read STDERR rather than STDOUT.
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if *g_debug {
|
|
log.Printf("build out: %s\n", string(out))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// executes autobuild function if autobuild option is enabled, logs error and
|
|
// ignores it
|
|
func try_autobuild(p *build.Package) {
|
|
if g_config.Autobuild {
|
|
err := autobuild(p)
|
|
if err != nil && *g_debug {
|
|
log.Printf("Autobuild error: %s\n", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func log_found_package_maybe(imp, pkgpath string) {
|
|
if *g_debug {
|
|
log.Printf("Found %q at %q\n", imp, pkgpath)
|
|
}
|
|
}
|
|
|
|
func log_build_context(context *package_lookup_context) {
|
|
log.Printf(" GOROOT: %s\n", context.GOROOT)
|
|
log.Printf(" GOPATH: %s\n", context.GOPATH)
|
|
log.Printf(" GOOS: %s\n", context.GOOS)
|
|
log.Printf(" GOARCH: %s\n", context.GOARCH)
|
|
log.Printf(" BzlProjectRoot: %q\n", context.BzlProjectRoot)
|
|
log.Printf(" GBProjectRoot: %q\n", context.GBProjectRoot)
|
|
log.Printf(" lib-path: %q\n", g_config.LibPath)
|
|
}
|
|
|
|
// find_global_file returns the file path of the compiled package corresponding to the specified
|
|
// import, and a boolean stating whether such path is valid.
|
|
// TODO: Return only one value, possibly empty string if not found.
|
|
func find_global_file(imp string, context *package_lookup_context) (string, bool) {
|
|
// gocode synthetically generates the builtin package
|
|
// "unsafe", since the "unsafe.a" package doesn't really exist.
|
|
// Thus, when the user request for the package "unsafe" we
|
|
// would return synthetic global file that would be used
|
|
// just as a key name to find this synthetic package
|
|
if imp == "unsafe" {
|
|
return "unsafe", true
|
|
}
|
|
|
|
pkgfile := fmt.Sprintf("%s.a", imp)
|
|
|
|
// if lib-path is defined, use it
|
|
if g_config.LibPath != "" {
|
|
for _, p := range filepath.SplitList(g_config.LibPath) {
|
|
pkg_path := filepath.Join(p, pkgfile)
|
|
if file_exists(pkg_path) {
|
|
log_found_package_maybe(imp, pkg_path)
|
|
return pkg_path, true
|
|
}
|
|
// Also check the relevant pkg/OS_ARCH dir for the libpath, if provided.
|
|
pkgdir := fmt.Sprintf("%s_%s", context.GOOS, context.GOARCH)
|
|
pkg_path = filepath.Join(p, "pkg", pkgdir, pkgfile)
|
|
if file_exists(pkg_path) {
|
|
log_found_package_maybe(imp, pkg_path)
|
|
return pkg_path, true
|
|
}
|
|
}
|
|
}
|
|
|
|
// gb-specific lookup mode, only if the root dir was found
|
|
if g_config.PackageLookupMode == "gb" && context.GBProjectRoot != "" {
|
|
root := context.GBProjectRoot
|
|
pkg_path := filepath.Join(root, "pkg", context.GOOS+"-"+context.GOARCH, pkgfile)
|
|
if file_exists(pkg_path) {
|
|
log_found_package_maybe(imp, pkg_path)
|
|
return pkg_path, true
|
|
}
|
|
}
|
|
|
|
// bzl-specific lookup mode, only if the root dir was found
|
|
if g_config.PackageLookupMode == "bzl" && context.BzlProjectRoot != "" {
|
|
var root, impath string
|
|
if strings.HasPrefix(imp, g_config.CustomPkgPrefix+"/") {
|
|
root = filepath.Join(context.BzlProjectRoot, "bazel-bin")
|
|
impath = imp[len(g_config.CustomPkgPrefix)+1:]
|
|
} else if g_config.CustomVendorDir != "" {
|
|
// Try custom vendor dir.
|
|
root = filepath.Join(context.BzlProjectRoot, "bazel-bin", g_config.CustomVendorDir)
|
|
impath = imp
|
|
}
|
|
|
|
if root != "" && impath != "" {
|
|
// There might be more than one ".a" files in the pkg path with bazel.
|
|
// But the best practice is to keep one go_library build target in each
|
|
// pakcage directory so that it follows the standard Go package
|
|
// structure. Thus here we assume there is at most one ".a" file existing
|
|
// in the pkg path.
|
|
if d, err := os.Open(filepath.Join(root, impath)); err == nil {
|
|
defer d.Close()
|
|
|
|
if fis, err := d.Readdir(-1); err == nil {
|
|
for _, fi := range fis {
|
|
if !fi.IsDir() && filepath.Ext(fi.Name()) == ".a" {
|
|
pkg_path := filepath.Join(root, impath, fi.Name())
|
|
log_found_package_maybe(imp, pkg_path)
|
|
return pkg_path, true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if context.CurrentPackagePath != "" {
|
|
// Try vendor path first, see GO15VENDOREXPERIMENT.
|
|
// We don't check this environment variable however, seems like there is
|
|
// almost no harm in doing so (well.. if you experiment with vendoring,
|
|
// gocode will fail after enabling/disabling the flag, and you'll be
|
|
// forced to get rid of vendor binaries). But asking users to set this
|
|
// env var is up will bring more trouble. Because we also need to pass
|
|
// it from client to server, make sure their editors set it, etc.
|
|
// So, whatever, let's just pretend it's always on.
|
|
package_path := context.CurrentPackagePath
|
|
for {
|
|
limp := filepath.Join(package_path, "vendor", imp)
|
|
if p, err := context.Import(limp, "", build.AllowBinary|build.FindOnly); err == nil {
|
|
try_autobuild(p)
|
|
if file_exists(p.PkgObj) {
|
|
log_found_package_maybe(imp, p.PkgObj)
|
|
return p.PkgObj, true
|
|
}
|
|
}
|
|
if package_path == "" {
|
|
break
|
|
}
|
|
next_path := filepath.Dir(package_path)
|
|
// let's protect ourselves from inf recursion here
|
|
if next_path == package_path {
|
|
break
|
|
}
|
|
package_path = next_path
|
|
}
|
|
}
|
|
|
|
if p, err := context.Import(imp, "", build.AllowBinary|build.FindOnly); err == nil {
|
|
try_autobuild(p)
|
|
if file_exists(p.PkgObj) {
|
|
log_found_package_maybe(imp, p.PkgObj)
|
|
return p.PkgObj, true
|
|
}
|
|
}
|
|
|
|
if *g_debug {
|
|
log.Printf("Import path %q was not resolved\n", imp)
|
|
log.Println("Gocode's build context is:")
|
|
log_build_context(context)
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func package_name(file *ast.File) string {
|
|
if file.Name != nil {
|
|
return file.Name.Name
|
|
}
|
|
return ""
|
|
}
|
|
|
|
//-------------------------------------------------------------------------
|
|
// decl_cache
|
|
//
|
|
// Thread-safe collection of DeclFileCache entities.
|
|
//-------------------------------------------------------------------------
|
|
|
|
type package_lookup_context struct {
|
|
build.Context
|
|
BzlProjectRoot string
|
|
GBProjectRoot string
|
|
CurrentPackagePath string
|
|
}
|
|
|
|
// gopath returns the list of Go path directories.
|
|
func (ctxt *package_lookup_context) gopath() []string {
|
|
var all []string
|
|
for _, p := range filepath.SplitList(ctxt.GOPATH) {
|
|
if p == "" || p == ctxt.GOROOT {
|
|
// Empty paths are uninteresting.
|
|
// If the path is the GOROOT, ignore it.
|
|
// People sometimes set GOPATH=$GOROOT.
|
|
// Do not get confused by this common mistake.
|
|
continue
|
|
}
|
|
if strings.HasPrefix(p, "~") {
|
|
// Path segments starting with ~ on Unix are almost always
|
|
// users who have incorrectly quoted ~ while setting GOPATH,
|
|
// preventing it from expanding to $HOME.
|
|
// The situation is made more confusing by the fact that
|
|
// bash allows quoted ~ in $PATH (most shells do not).
|
|
// Do not get confused by this, and do not try to use the path.
|
|
// It does not exist, and printing errors about it confuses
|
|
// those users even more, because they think "sure ~ exists!".
|
|
// The go command diagnoses this situation and prints a
|
|
// useful error.
|
|
// On Windows, ~ is used in short names, such as c:\progra~1
|
|
// for c:\program files.
|
|
continue
|
|
}
|
|
all = append(all, p)
|
|
}
|
|
return all
|
|
}
|
|
|
|
func (ctxt *package_lookup_context) pkg_dirs() []string {
|
|
pkgdir := fmt.Sprintf("%s_%s", ctxt.GOOS, ctxt.GOARCH)
|
|
|
|
var all []string
|
|
if ctxt.GOROOT != "" {
|
|
dir := filepath.Join(ctxt.GOROOT, "pkg", pkgdir)
|
|
if is_dir(dir) {
|
|
all = append(all, dir)
|
|
}
|
|
}
|
|
|
|
switch g_config.PackageLookupMode {
|
|
case "go":
|
|
for _, p := range ctxt.gopath() {
|
|
dir := filepath.Join(p, "pkg", pkgdir)
|
|
if is_dir(dir) {
|
|
all = append(all, dir)
|
|
}
|
|
}
|
|
case "gb":
|
|
if ctxt.GBProjectRoot != "" {
|
|
pkgdir := fmt.Sprintf("%s-%s", ctxt.GOOS, ctxt.GOARCH)
|
|
dir := filepath.Join(ctxt.GBProjectRoot, "pkg", pkgdir)
|
|
if is_dir(dir) {
|
|
all = append(all, dir)
|
|
}
|
|
}
|
|
case "bzl":
|
|
// TODO: Support bazel mode
|
|
}
|
|
return all
|
|
}
|
|
|
|
type decl_cache struct {
|
|
cache map[string]*decl_file_cache
|
|
context *package_lookup_context
|
|
sync.Mutex
|
|
}
|
|
|
|
func new_decl_cache(context *package_lookup_context) *decl_cache {
|
|
return &decl_cache{
|
|
cache: make(map[string]*decl_file_cache),
|
|
context: context,
|
|
}
|
|
}
|
|
|
|
func (c *decl_cache) get(filename string) *decl_file_cache {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
|
|
f, ok := c.cache[filename]
|
|
if !ok {
|
|
f = new_decl_file_cache(filename, c.context)
|
|
c.cache[filename] = f
|
|
}
|
|
return f
|
|
}
|
|
|
|
func (c *decl_cache) get_and_update(filename string) *decl_file_cache {
|
|
f := c.get(filename)
|
|
f.update()
|
|
return f
|
|
}
|