github.com/traefik/yaegi@v0.15.1/interp/src.go (about)

     1  package interp
     2  
     3  import (
     4  	"fmt"
     5  	"io/fs"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  )
    10  
    11  // importSrc calls gta on the source code for the package identified by
    12  // importPath. rPath is the relative path to the directory containing the source
    13  // code for the package. It can also be "main" as a special value.
    14  func (interp *Interpreter) importSrc(rPath, importPath string, skipTest bool) (string, error) {
    15  	var dir string
    16  	var err error
    17  
    18  	if interp.srcPkg[importPath] != nil {
    19  		name, ok := interp.pkgNames[importPath]
    20  		if !ok {
    21  			return "", fmt.Errorf("inconsistent knowledge about %s", importPath)
    22  		}
    23  		return name, nil
    24  	}
    25  
    26  	// For relative import paths in the form "./xxx" or "../xxx", the initial
    27  	// base path is the directory of the interpreter input file, or "." if no file
    28  	// was provided.
    29  	// In all other cases, absolute import paths are resolved from the GOPATH
    30  	// and the nested "vendor" directories.
    31  	if isPathRelative(importPath) {
    32  		if rPath == mainID {
    33  			rPath = "."
    34  		}
    35  		dir = filepath.Join(filepath.Dir(interp.name), rPath, importPath)
    36  	} else if dir, rPath, err = interp.pkgDir(interp.context.GOPATH, rPath, importPath); err != nil {
    37  		// Try again, assuming a root dir at the source location.
    38  		if rPath, err = interp.rootFromSourceLocation(); err != nil {
    39  			return "", err
    40  		}
    41  		if dir, rPath, err = interp.pkgDir(interp.context.GOPATH, rPath, importPath); err != nil {
    42  			return "", err
    43  		}
    44  	}
    45  
    46  	if interp.rdir[importPath] {
    47  		return "", fmt.Errorf("import cycle not allowed\n\timports %s", importPath)
    48  	}
    49  	interp.rdir[importPath] = true
    50  
    51  	files, err := fs.ReadDir(interp.opt.filesystem, dir)
    52  	if err != nil {
    53  		return "", err
    54  	}
    55  
    56  	var initNodes []*node
    57  	var rootNodes []*node
    58  	revisit := make(map[string][]*node)
    59  
    60  	var root *node
    61  	var pkgName string
    62  
    63  	// Parse source files.
    64  	for _, file := range files {
    65  		name := file.Name()
    66  		if skipFile(&interp.context, name, skipTest) {
    67  			continue
    68  		}
    69  
    70  		name = filepath.Join(dir, name)
    71  		var buf []byte
    72  		if buf, err = fs.ReadFile(interp.opt.filesystem, name); err != nil {
    73  			return "", err
    74  		}
    75  
    76  		n, err := interp.parse(string(buf), name, false)
    77  		if err != nil {
    78  			return "", err
    79  		}
    80  		if n == nil {
    81  			continue
    82  		}
    83  
    84  		var pname string
    85  		if pname, root, err = interp.ast(n); err != nil {
    86  			return "", err
    87  		}
    88  		if root == nil {
    89  			continue
    90  		}
    91  
    92  		if interp.astDot {
    93  			dotCmd := interp.dotCmd
    94  			if dotCmd == "" {
    95  				dotCmd = defaultDotCmd(name, "yaegi-ast-")
    96  			}
    97  			root.astDot(dotWriter(dotCmd), name)
    98  		}
    99  		if pkgName == "" {
   100  			pkgName = pname
   101  		} else if pkgName != pname && skipTest {
   102  			return "", fmt.Errorf("found packages %s and %s in %s", pkgName, pname, dir)
   103  		}
   104  		rootNodes = append(rootNodes, root)
   105  
   106  		subRPath := effectivePkg(rPath, importPath)
   107  		var list []*node
   108  		list, err = interp.gta(root, subRPath, importPath, pkgName)
   109  		if err != nil {
   110  			return "", err
   111  		}
   112  		revisit[subRPath] = append(revisit[subRPath], list...)
   113  	}
   114  
   115  	// Revisit incomplete nodes where GTA could not complete.
   116  	for _, nodes := range revisit {
   117  		if err = interp.gtaRetry(nodes, importPath, pkgName); err != nil {
   118  			return "", err
   119  		}
   120  	}
   121  
   122  	// Generate control flow graphs.
   123  	for _, root := range rootNodes {
   124  		var nodes []*node
   125  		if nodes, err = interp.cfg(root, nil, importPath, pkgName); err != nil {
   126  			return "", err
   127  		}
   128  		initNodes = append(initNodes, nodes...)
   129  	}
   130  
   131  	// Register source package in the interpreter. The package contains only
   132  	// the global symbols in the package scope.
   133  	interp.mutex.Lock()
   134  	gs := interp.scopes[importPath]
   135  	if gs == nil {
   136  		interp.mutex.Unlock()
   137  		// A nil scope means that no even an empty package is created from source.
   138  		return "", fmt.Errorf("no Go files in %s", dir)
   139  	}
   140  	interp.srcPkg[importPath] = gs.sym
   141  	interp.pkgNames[importPath] = pkgName
   142  
   143  	interp.frame.mutex.Lock()
   144  	interp.resizeFrame()
   145  	interp.frame.mutex.Unlock()
   146  	interp.mutex.Unlock()
   147  
   148  	// Once all package sources have been parsed, execute entry points then init functions.
   149  	for _, n := range rootNodes {
   150  		if err = genRun(n); err != nil {
   151  			return "", err
   152  		}
   153  		interp.run(n, nil)
   154  	}
   155  
   156  	// Wire and execute global vars in global scope gs.
   157  	n, err := genGlobalVars(rootNodes, gs)
   158  	if err != nil {
   159  		return "", err
   160  	}
   161  	interp.run(n, nil)
   162  
   163  	// Add main to list of functions to run, after all inits.
   164  	if m := gs.sym[mainID]; pkgName == mainID && m != nil && skipTest {
   165  		initNodes = append(initNodes, m.node)
   166  	}
   167  
   168  	for _, n := range initNodes {
   169  		interp.run(n, interp.frame)
   170  	}
   171  
   172  	return pkgName, nil
   173  }
   174  
   175  // rootFromSourceLocation returns the path to the directory containing the input
   176  // Go file given to the interpreter, relative to $GOPATH/src.
   177  // It is meant to be called in the case when the initial input is a main package.
   178  func (interp *Interpreter) rootFromSourceLocation() (string, error) {
   179  	sourceFile := interp.name
   180  	if sourceFile == DefaultSourceName {
   181  		return "", nil
   182  	}
   183  	wd, err := os.Getwd()
   184  	if err != nil {
   185  		return "", err
   186  	}
   187  	pkgDir := filepath.Join(wd, filepath.Dir(sourceFile))
   188  	root := strings.TrimPrefix(pkgDir, filepath.Join(interp.context.GOPATH, "src")+"/")
   189  	if root == wd {
   190  		return "", fmt.Errorf("package location %s not in GOPATH", pkgDir)
   191  	}
   192  	return root, nil
   193  }
   194  
   195  // pkgDir returns the absolute path in filesystem for a package given its import path
   196  // and the root of the subtree dependencies.
   197  func (interp *Interpreter) pkgDir(goPath string, root, importPath string) (string, string, error) {
   198  	rPath := filepath.Join(root, "vendor")
   199  	dir := filepath.Join(goPath, "src", rPath, importPath)
   200  
   201  	if _, err := fs.Stat(interp.opt.filesystem, dir); err == nil {
   202  		return dir, rPath, nil // found!
   203  	}
   204  
   205  	dir = filepath.Join(goPath, "src", effectivePkg(root, importPath))
   206  
   207  	if _, err := fs.Stat(interp.opt.filesystem, dir); err == nil {
   208  		return dir, root, nil // found!
   209  	}
   210  
   211  	if len(root) == 0 {
   212  		if interp.context.GOPATH == "" {
   213  			return "", "", fmt.Errorf("unable to find source related to: %q. Either the GOPATH environment variable, or the Interpreter.Options.GoPath needs to be set", importPath)
   214  		}
   215  		return "", "", fmt.Errorf("unable to find source related to: %q", importPath)
   216  	}
   217  
   218  	rootPath := filepath.Join(goPath, "src", root)
   219  	prevRoot, err := previousRoot(interp.opt.filesystem, rootPath, root)
   220  	if err != nil {
   221  		return "", "", err
   222  	}
   223  
   224  	return interp.pkgDir(goPath, prevRoot, importPath)
   225  }
   226  
   227  const vendor = "vendor"
   228  
   229  // Find the previous source root (vendor > vendor > ... > GOPATH).
   230  func previousRoot(filesystem fs.FS, rootPath, root string) (string, error) {
   231  	rootPath = filepath.Clean(rootPath)
   232  	parent, final := filepath.Split(rootPath)
   233  	parent = filepath.Clean(parent)
   234  
   235  	// TODO(mpl): maybe it works for the special case main, but can't be bothered for now.
   236  	if root != mainID && final != vendor {
   237  		root = strings.TrimSuffix(root, string(filepath.Separator))
   238  		prefix := strings.TrimSuffix(strings.TrimSuffix(rootPath, root), string(filepath.Separator))
   239  
   240  		// look for the closest vendor in one of our direct ancestors, as it takes priority.
   241  		var vendored string
   242  		for {
   243  			fi, err := fs.Stat(filesystem, filepath.Join(parent, vendor))
   244  			if err == nil && fi.IsDir() {
   245  				vendored = strings.TrimPrefix(strings.TrimPrefix(parent, prefix), string(filepath.Separator))
   246  				break
   247  			}
   248  			if !os.IsNotExist(err) {
   249  				return "", err
   250  			}
   251  			// stop when we reach GOPATH/src
   252  			if parent == prefix {
   253  				break
   254  			}
   255  
   256  			// stop when we reach GOPATH/src/blah
   257  			parent = filepath.Dir(parent)
   258  			if parent == prefix {
   259  				break
   260  			}
   261  
   262  			// just an additional failsafe, stop if we reach the filesystem root, or dot (if
   263  			// we are dealing with relative paths).
   264  			// TODO(mpl): It should probably be a critical error actually,
   265  			// as we shouldn't have gone that high up in the tree.
   266  			// TODO(dennwc): This partially fails on Windows, since it cannot recognize drive letters as "root".
   267  			if parent == string(filepath.Separator) || parent == "." || parent == "" {
   268  				break
   269  			}
   270  		}
   271  
   272  		if vendored != "" {
   273  			return vendored, nil
   274  		}
   275  	}
   276  
   277  	// TODO(mpl): the algorithm below might be redundant with the one above,
   278  	// but keeping it for now. Investigate/simplify/remove later.
   279  	splitRoot := strings.Split(root, string(filepath.Separator))
   280  	var index int
   281  	for i := len(splitRoot) - 1; i >= 0; i-- {
   282  		if splitRoot[i] == "vendor" {
   283  			index = i
   284  			break
   285  		}
   286  	}
   287  
   288  	if index == 0 {
   289  		return "", nil
   290  	}
   291  
   292  	return filepath.Join(splitRoot[:index]...), nil
   293  }
   294  
   295  func effectivePkg(root, path string) string {
   296  	splitRoot := strings.Split(root, string(filepath.Separator))
   297  	splitPath := strings.Split(path, string(filepath.Separator))
   298  
   299  	var result []string
   300  
   301  	rootIndex := 0
   302  	prevRootIndex := 0
   303  	for i := 0; i < len(splitPath); i++ {
   304  		part := splitPath[len(splitPath)-1-i]
   305  
   306  		index := len(splitRoot) - 1 - rootIndex
   307  		if index > 0 && part == splitRoot[index] && i != 0 {
   308  			prevRootIndex = rootIndex
   309  			rootIndex++
   310  		} else if prevRootIndex == rootIndex {
   311  			result = append(result, part)
   312  		}
   313  	}
   314  
   315  	var frag string
   316  	for i := len(result) - 1; i >= 0; i-- {
   317  		frag = filepath.Join(frag, result[i])
   318  	}
   319  
   320  	return filepath.Join(root, frag)
   321  }
   322  
   323  // isPathRelative returns true if path starts with "./" or "../".
   324  // It is intended for use on import paths, where "/" is always the directory separator.
   325  func isPathRelative(s string) bool {
   326  	return strings.HasPrefix(s, "./") || strings.HasPrefix(s, "../")
   327  }