The recent changes that added some redeclaration checks implicitly added more strictness related to namespaces and scopes which, among other things, broke some uses that "accidentally" used to work. For example, given const script1 = ` import "fmt" // more code ` const script2 = ` import "fmt" // some other code ` If one Evals script1, then script2, with the same interpreter, without specifying any scope, as the two fragments would be considered part of the same (.go file) scope by default, a redeclaration error would be triggered because import "fmt" is seen twice. A work-around would have been to specify (a different) i.Name before each Eval call, so that each script is considered as coming from a different .go file, and hence are respectively in different scopes with respect to imports. That lead us to realize we had to make specifying things such as file-scope, and "incremental mode" (aka REPL), more obvious in the context of an Eval call. In addition, we want to lay down the foundations for Yaegi being able to behave more like the go tool wrt to various inputs, i.e. it should be able to take a package directory, or an import path, as input, instead of just a .go file. Hence the introduction of a new kind of Eval method (whose signature is not fixed yet): func (interp *Interpreter) EvalPath(path string) (res reflect.Value, err error) It partially solves the problem described above because: 1. the path given to EvalPath can be used as the file-scope hint mentioned above, for now (even though the related implementation details might change). 2. Eval always runs in incremental mode, whereas EvalPath always runs in non-incremental mode, hence clarifying the situation in that respect. And to avoid confusion, the Name field of Interpreter is now non-exported, since it is somewhat redundant with the path argument of EvalPath. Note that #731 is not fully fixed (and might never be), as a requirement of the proposed solution is to move the input bits of code into respective files (instead of leaving them as strings). Finally, some related bugfixes, documention changes, and some refactoring have been included. Notably, there is no "empty scope" anymore, i.e. name defaults to "_.go" when it is not specified. Updates #731 Fixes #778 Fixes #798 Fixes #789 Co-authored-by: Marc Vertes <mvertes@free.fr>
311 lines
8.0 KiB
Go
311 lines
8.0 KiB
Go
package interp
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// importSrc calls gta on the source code for the package identified by
|
|
// importPath. rPath is the relative path to the directory containing the source
|
|
// code for the package. It can also be "main" as a special value.
|
|
func (interp *Interpreter) importSrc(rPath, importPath string) (string, error) {
|
|
var dir string
|
|
var err error
|
|
|
|
if interp.srcPkg[importPath] != nil {
|
|
name, ok := interp.pkgNames[importPath]
|
|
if !ok {
|
|
return "", fmt.Errorf("inconsistent knowledge about %s", importPath)
|
|
}
|
|
return name, nil
|
|
}
|
|
|
|
// For relative import paths in the form "./xxx" or "../xxx", the initial
|
|
// base path is the directory of the interpreter input file, or "." if no file
|
|
// was provided.
|
|
// In all other cases, absolute import paths are resolved from the GOPATH
|
|
// and the nested "vendor" directories.
|
|
if isPathRelative(importPath) {
|
|
if rPath == mainID {
|
|
rPath = "."
|
|
}
|
|
dir = filepath.Join(filepath.Dir(interp.name), rPath, importPath)
|
|
} else {
|
|
var root string
|
|
if rPath == mainID {
|
|
root, err = interp.rootFromSourceLocation()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
} else {
|
|
root = rPath
|
|
}
|
|
if dir, rPath, err = pkgDir(interp.context.GOPATH, root, importPath); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
if interp.rdir[importPath] {
|
|
return "", fmt.Errorf("import cycle not allowed\n\timports %s", importPath)
|
|
}
|
|
interp.rdir[importPath] = true
|
|
|
|
files, err := ioutil.ReadDir(dir)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var initNodes []*node
|
|
var rootNodes []*node
|
|
revisit := make(map[string][]*node)
|
|
|
|
var root *node
|
|
var pkgName string
|
|
|
|
// Parse source files.
|
|
for _, file := range files {
|
|
name := file.Name()
|
|
if skipFile(&interp.context, name) {
|
|
continue
|
|
}
|
|
|
|
name = filepath.Join(dir, name)
|
|
var buf []byte
|
|
if buf, err = ioutil.ReadFile(name); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var pname string
|
|
if pname, root, err = interp.ast(string(buf), name, false); err != nil {
|
|
return "", err
|
|
}
|
|
if root == nil {
|
|
continue
|
|
}
|
|
|
|
if interp.astDot {
|
|
dotCmd := interp.dotCmd
|
|
if dotCmd == "" {
|
|
dotCmd = defaultDotCmd(name, "yaegi-ast-")
|
|
}
|
|
root.astDot(dotWriter(dotCmd), name)
|
|
}
|
|
if pkgName == "" {
|
|
pkgName = pname
|
|
} else if pkgName != pname {
|
|
return "", fmt.Errorf("found packages %s and %s in %s", pkgName, pname, dir)
|
|
}
|
|
rootNodes = append(rootNodes, root)
|
|
|
|
subRPath := effectivePkg(rPath, importPath)
|
|
var list []*node
|
|
list, err = interp.gta(root, subRPath, importPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
revisit[subRPath] = append(revisit[subRPath], list...)
|
|
}
|
|
|
|
// Revisit incomplete nodes where GTA could not complete.
|
|
for _, nodes := range revisit {
|
|
if err = interp.gtaRetry(nodes, importPath); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
// Generate control flow graphs
|
|
for _, root := range rootNodes {
|
|
var nodes []*node
|
|
if nodes, err = interp.cfg(root, importPath); err != nil {
|
|
return "", err
|
|
}
|
|
initNodes = append(initNodes, nodes...)
|
|
}
|
|
|
|
// Register source package in the interpreter. The package contains only
|
|
// the global symbols in the package scope.
|
|
interp.mutex.Lock()
|
|
interp.srcPkg[importPath] = interp.scopes[importPath].sym
|
|
interp.pkgNames[importPath] = pkgName
|
|
|
|
interp.frame.mutex.Lock()
|
|
interp.resizeFrame()
|
|
interp.frame.mutex.Unlock()
|
|
interp.mutex.Unlock()
|
|
|
|
// Once all package sources have been parsed, execute entry points then init functions
|
|
for _, n := range rootNodes {
|
|
if err = genRun(n); err != nil {
|
|
return "", err
|
|
}
|
|
interp.run(n, nil)
|
|
}
|
|
|
|
// Wire and execute global vars
|
|
n, err := genGlobalVars(rootNodes, interp.scopes[importPath])
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
interp.run(n, nil)
|
|
|
|
// Add main to list of functions to run, after all inits
|
|
if m := interp.main(); m != nil {
|
|
initNodes = append(initNodes, m)
|
|
}
|
|
|
|
for _, n := range initNodes {
|
|
interp.run(n, interp.frame)
|
|
}
|
|
|
|
return pkgName, nil
|
|
}
|
|
|
|
// rootFromSourceLocation returns the path to the directory containing the input
|
|
// Go file given to the interpreter, relative to $GOPATH/src.
|
|
// It is meant to be called in the case when the initial input is a main package.
|
|
func (interp *Interpreter) rootFromSourceLocation() (string, error) {
|
|
sourceFile := interp.name
|
|
if sourceFile == DefaultSourceName {
|
|
return "", nil
|
|
}
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
pkgDir := filepath.Join(wd, filepath.Dir(sourceFile))
|
|
root := strings.TrimPrefix(pkgDir, filepath.Join(interp.context.GOPATH, "src")+"/")
|
|
if root == wd {
|
|
return "", fmt.Errorf("package location %s not in GOPATH", pkgDir)
|
|
}
|
|
return root, nil
|
|
}
|
|
|
|
// pkgDir returns the absolute path in filesystem for a package given its import path
|
|
// and the root of the subtree dependencies.
|
|
func pkgDir(goPath string, root, importPath string) (string, string, error) {
|
|
rPath := filepath.Join(root, "vendor")
|
|
dir := filepath.Join(goPath, "src", rPath, importPath)
|
|
|
|
if _, err := os.Stat(dir); err == nil {
|
|
return dir, rPath, nil // found!
|
|
}
|
|
|
|
dir = filepath.Join(goPath, "src", effectivePkg(root, importPath))
|
|
|
|
if _, err := os.Stat(dir); err == nil {
|
|
return dir, root, nil // found!
|
|
}
|
|
|
|
if len(root) == 0 {
|
|
return "", "", fmt.Errorf("unable to find source related to: %q", importPath)
|
|
}
|
|
|
|
rootPath := filepath.Join(goPath, "src", root)
|
|
prevRoot, err := previousRoot(rootPath, root)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
return pkgDir(goPath, prevRoot, importPath)
|
|
}
|
|
|
|
const vendor = "vendor"
|
|
|
|
// Find the previous source root (vendor > vendor > ... > GOPATH).
|
|
func previousRoot(rootPath, root string) (string, error) {
|
|
rootPath = filepath.Clean(rootPath)
|
|
parent, final := filepath.Split(rootPath)
|
|
parent = filepath.Clean(parent)
|
|
|
|
// TODO(mpl): maybe it works for the special case main, but can't be bothered for now.
|
|
if root != mainID && final != vendor {
|
|
root = strings.TrimSuffix(root, string(filepath.Separator))
|
|
prefix := strings.TrimSuffix(strings.TrimSuffix(rootPath, root), string(filepath.Separator))
|
|
|
|
// look for the closest vendor in one of our direct ancestors, as it takes priority.
|
|
var vendored string
|
|
for {
|
|
fi, err := os.Lstat(filepath.Join(parent, vendor))
|
|
if err == nil && fi.IsDir() {
|
|
vendored = strings.TrimPrefix(strings.TrimPrefix(parent, prefix), string(filepath.Separator))
|
|
break
|
|
}
|
|
if !os.IsNotExist(err) {
|
|
return "", err
|
|
}
|
|
|
|
// stop when we reach GOPATH/src/blah
|
|
parent = filepath.Dir(parent)
|
|
if parent == prefix {
|
|
break
|
|
}
|
|
|
|
// just an additional failsafe, stop if we reach the filesystem root, or dot (if
|
|
// we are dealing with relative paths).
|
|
// TODO(mpl): It should probably be a critical error actually,
|
|
// as we shouldn't have gone that high up in the tree.
|
|
if parent == string(filepath.Separator) || parent == "." {
|
|
break
|
|
}
|
|
}
|
|
|
|
if vendored != "" {
|
|
return vendored, nil
|
|
}
|
|
}
|
|
|
|
// TODO(mpl): the algorithm below might be redundant with the one above,
|
|
// but keeping it for now. Investigate/simplify/remove later.
|
|
splitRoot := strings.Split(root, string(filepath.Separator))
|
|
var index int
|
|
for i := len(splitRoot) - 1; i >= 0; i-- {
|
|
if splitRoot[i] == "vendor" {
|
|
index = i
|
|
break
|
|
}
|
|
}
|
|
|
|
if index == 0 {
|
|
return "", nil
|
|
}
|
|
|
|
return filepath.Join(splitRoot[:index]...), nil
|
|
}
|
|
|
|
func effectivePkg(root, path string) string {
|
|
splitRoot := strings.Split(root, string(filepath.Separator))
|
|
splitPath := strings.Split(path, string(filepath.Separator))
|
|
|
|
var result []string
|
|
|
|
rootIndex := 0
|
|
prevRootIndex := 0
|
|
for i := 0; i < len(splitPath); i++ {
|
|
part := splitPath[len(splitPath)-1-i]
|
|
|
|
index := len(splitRoot) - 1 - rootIndex
|
|
if index > 0 && part == splitRoot[index] && i != 0 {
|
|
prevRootIndex = rootIndex
|
|
rootIndex++
|
|
} else if prevRootIndex == rootIndex {
|
|
result = append(result, part)
|
|
}
|
|
}
|
|
|
|
var frag string
|
|
for i := len(result) - 1; i >= 0; i-- {
|
|
frag = filepath.Join(frag, result[i])
|
|
}
|
|
|
|
return filepath.Join(root, frag)
|
|
}
|
|
|
|
// isPathRelative returns true if path starts with "./" or "../".
|
|
func isPathRelative(s string) bool {
|
|
p := "." + string(filepath.Separator)
|
|
return strings.HasPrefix(s, p) || strings.HasPrefix(s, "."+p)
|
|
}
|