github.com/switchupcb/yaegi@v0.10.2/interp/src.go (about)

     1  package interp
     2  
     3  import (
     4  	"fmt"
     5  	"io/fs"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"golang.org/x/tools/go/packages"
    11  )
    12  
    13  // importSrc calls global tag analysis on the source code for the package identified by
    14  // importPath. rPath is the relative path to the directory containing the source
    15  // code for the package. It can also be "main" as a special value.
    16  func (interp *Interpreter) importSrc(rPath, importPath string, skipTest bool) (string, error) {
    17  	var dir string
    18  	var err error
    19  
    20  	if interp.srcPkg[importPath] != nil {
    21  		name, ok := interp.pkgNames[importPath]
    22  		if !ok {
    23  			return "", fmt.Errorf("inconsistent knowledge about %s", importPath)
    24  		}
    25  		return name, nil
    26  	}
    27  
    28  	// resolve relative and absolute import paths
    29  	if isPathRelative(importPath) {
    30  		if rPath == mainID {
    31  			rPath = "."
    32  		}
    33  		dir = filepath.Join(filepath.Dir(interp.name), rPath, importPath)
    34  	} else {
    35  		if dir, err = interp.getPackageDir(importPath); err != nil {
    36  			return "", err
    37  		}
    38  	}
    39  
    40  	if interp.rdir[importPath] {
    41  		return "", fmt.Errorf("import cycle not allowed\n\timports %s", importPath)
    42  	}
    43  	interp.rdir[importPath] = true
    44  
    45  	files, err := fs.ReadDir(interp.opt.filesystem, dir)
    46  	if err != nil {
    47  		return "", err
    48  	}
    49  
    50  	var initNodes []*node
    51  	var rootNodes []*node
    52  	revisit := make(map[string][]*node)
    53  
    54  	var root *node
    55  	var pkgName string
    56  
    57  	// Parse source files.
    58  	for _, file := range files {
    59  		name := file.Name()
    60  		if skipFile(&interp.context, name, skipTest) {
    61  			continue
    62  		}
    63  
    64  		name = filepath.Join(dir, name)
    65  		var buf []byte
    66  		if buf, err = fs.ReadFile(interp.opt.filesystem, name); err != nil {
    67  			return "", err
    68  		}
    69  
    70  		n, err := interp.parse(string(buf), name, false)
    71  		if err != nil {
    72  			return "", err
    73  		}
    74  		if n == nil {
    75  			continue
    76  		}
    77  
    78  		var pname string
    79  		if pname, root, err = interp.ast(n); err != nil {
    80  			return "", err
    81  		}
    82  		if root == nil {
    83  			continue
    84  		}
    85  
    86  		if interp.astDot {
    87  			dotCmd := interp.dotCmd
    88  			if dotCmd == "" {
    89  				dotCmd = defaultDotCmd(name, "yaegi-ast-")
    90  			}
    91  			root.astDot(dotWriter(dotCmd), name)
    92  		}
    93  		if pkgName == "" {
    94  			pkgName = pname
    95  		} else if pkgName != pname && skipTest {
    96  			return "", fmt.Errorf("found packages %s and %s in %s", pkgName, pname, dir)
    97  		}
    98  		rootNodes = append(rootNodes, root)
    99  
   100  		subRPath := effectivePkg(rPath, importPath)
   101  		var list []*node
   102  		list, err = interp.gta(root, subRPath, importPath, pkgName)
   103  		if err != nil {
   104  			return "", err
   105  		}
   106  		revisit[subRPath] = append(revisit[subRPath], list...)
   107  	}
   108  
   109  	// Revisit incomplete nodes where GTA could not complete.
   110  	for _, nodes := range revisit {
   111  		if err = interp.gtaRetry(nodes, importPath, pkgName); err != nil {
   112  			return "", err
   113  		}
   114  	}
   115  
   116  	// Generate control flow graphs.
   117  	for _, root := range rootNodes {
   118  		var nodes []*node
   119  		if nodes, err = interp.cfg(root, nil, importPath, pkgName); err != nil {
   120  			return "", err
   121  		}
   122  		initNodes = append(initNodes, nodes...)
   123  	}
   124  
   125  	// Register source package in the interpreter. The package contains only
   126  	// the global symbols in the package scope.
   127  	interp.mutex.Lock()
   128  	gs := interp.scopes[importPath]
   129  	interp.srcPkg[importPath] = gs.sym
   130  	interp.pkgNames[importPath] = pkgName
   131  
   132  	interp.frame.mutex.Lock()
   133  	interp.resizeFrame()
   134  	interp.frame.mutex.Unlock()
   135  	interp.mutex.Unlock()
   136  
   137  	// Once all package sources have been parsed, execute entry points then init functions.
   138  	for _, n := range rootNodes {
   139  		if err = genRun(n); err != nil {
   140  			return "", err
   141  		}
   142  		interp.run(n, nil)
   143  	}
   144  
   145  	// Wire and execute global vars in global scope gs.
   146  	n, err := genGlobalVars(rootNodes, gs)
   147  	if err != nil {
   148  		return "", err
   149  	}
   150  	interp.run(n, nil)
   151  
   152  	// Add main to list of functions to run, after all inits.
   153  	if m := gs.sym[mainID]; pkgName == mainID && m != nil && skipTest {
   154  		initNodes = append(initNodes, m.node)
   155  	}
   156  
   157  	for _, n := range initNodes {
   158  		interp.run(n, interp.frame)
   159  	}
   160  
   161  	return pkgName, nil
   162  }
   163  
   164  // rootFromSourceLocation returns the path to the directory containing the input
   165  // Go file given to the interpreter, relative to $GOPATH/src.
   166  // It is meant to be called in the case when the initial input is a main package.
   167  func (interp *Interpreter) rootFromSourceLocation() (string, error) {
   168  	sourceFile := interp.name
   169  	if sourceFile == DefaultSourceName {
   170  		return "", nil
   171  	}
   172  	wd, err := os.Getwd()
   173  	if err != nil {
   174  		return "", err
   175  	}
   176  	pkgDir := filepath.Join(wd, filepath.Dir(sourceFile))
   177  	root := strings.TrimPrefix(pkgDir, filepath.Join(interp.context.GOPATH, "src")+"/")
   178  	if root == wd {
   179  		return "", fmt.Errorf("package location %s not in GOPATH", pkgDir)
   180  	}
   181  	return root, nil
   182  }
   183  
   184  // getPackageDir uses the GOPATH to find the absolute path of an import path
   185  func (interp *Interpreter) getPackageDir(importPath string) (string, error) {
   186  	// search the standard library and Go modules.
   187  	config := packages.Config{}
   188  	config.Env = append(config.Env, "GOPATH="+interp.context.GOPATH, "GOCACHE="+interp.opt.goCache, "GOTOOLDIR="+interp.opt.goToolDir)
   189  	pkgs, err := packages.Load(&config, importPath)
   190  	if err != nil {
   191  		return "", fmt.Errorf("An error occurred retrieving a package from the GOPATH: %v\n%v\nIf Access is denied, run in administrator.", importPath, err)
   192  	}
   193  
   194  	// confirm the import path is found.
   195  	for _, pkg := range pkgs {
   196  		for _, goFile := range pkg.GoFiles {
   197  			if strings.Contains(filepath.Dir(goFile), pkg.Name) {
   198  				return filepath.Dir(goFile), nil
   199  			}
   200  		}
   201  	}
   202  
   203  	// check for certain go tools located in GOTOOLDIR
   204  	if interp.opt.goToolDir != "" {
   205  		// search for the go directory before searching for packages
   206  		// this approach prevents the computer from searching the entire filesystem
   207  		godir, err := searchUpDirPath(interp.opt.goToolDir, "go", false)
   208  		if err != nil {
   209  			return "", fmt.Errorf("An import source could not be found: %q\nThe current GOPATH=%v, GOCACHE=%v, GOTOOLDIR=%v\n%v", importPath, interp.context.GOPATH, interp.opt.goCache, interp.opt.goToolDir, err)
   210  		}
   211  
   212  		absimportpath, err := searchDirs(godir, importPath)
   213  		if err != nil {
   214  			return "", fmt.Errorf("An import source could not be found: %q\nThe current GOPATH=%v, GOCACHE=%v, GOTOOLDIR=%v\n%v", importPath, interp.context.GOPATH, interp.opt.goCache, interp.opt.goToolDir, err)
   215  		}
   216  		return absimportpath, nil
   217  	}
   218  	return "", fmt.Errorf("An import source could not be found: %q. Set the GOPATH and/or GOTOOLDIR environment variable from Interpreter.Options.", importPath)
   219  }
   220  
   221  // searchUpDirPath searches up a directory path in order to find a target directory.
   222  func searchUpDirPath(initial string, target string, isCaseSensitive bool) (string, error) {
   223  	// strings.Split always returns [:0] as filepath.Dir returns "." or the last directory
   224  	splitdir := strings.Split(filepath.Join(initial), string(filepath.Separator))
   225  	if len(splitdir) == 1 {
   226  		return "", fmt.Errorf("The target directory %q is not within the path %q", target, initial)
   227  	}
   228  
   229  	updir := splitdir[len(splitdir)-1]
   230  	if !isCaseSensitive {
   231  		updir = strings.ToLower(updir)
   232  	}
   233  	if updir == target {
   234  		return initial, nil
   235  	}
   236  	return searchUpDirPath(filepath.Dir(initial), target, isCaseSensitive)
   237  }
   238  
   239  // searchDirs searches within a directory (and its subdirectories) in an attempt to find a filepath.
   240  func searchDirs(initial string, target string) (string, error) {
   241  	absfilepath, err := filepath.Abs(initial)
   242  	if err != nil {
   243  		return "", err
   244  	}
   245  
   246  	// find the go directory
   247  	var foundpath string
   248  	filter := func(path string, d fs.DirEntry, err error) error {
   249  		if d.IsDir() {
   250  			if d.Name() == target {
   251  				foundpath = path
   252  			}
   253  		}
   254  		return nil
   255  	}
   256  	if err = filepath.WalkDir(absfilepath, filter); err != nil {
   257  		return "", fmt.Errorf("An error occurred searching for a directory.\n%v", err)
   258  	}
   259  
   260  	if foundpath != "" {
   261  		return foundpath, nil
   262  	}
   263  	return "", fmt.Errorf("The target filepath %q is not within the path %q", target, initial)
   264  }
   265  
   266  func effectivePkg(root, path string) string {
   267  	splitRoot := strings.Split(root, string(filepath.Separator))
   268  	splitPath := strings.Split(path, string(filepath.Separator))
   269  
   270  	var result []string
   271  
   272  	rootIndex := 0
   273  	prevRootIndex := 0
   274  	for i := 0; i < len(splitPath); i++ {
   275  		part := splitPath[len(splitPath)-1-i]
   276  
   277  		index := len(splitRoot) - 1 - rootIndex
   278  		if index > 0 && part == splitRoot[index] && i != 0 {
   279  			prevRootIndex = rootIndex
   280  			rootIndex++
   281  		} else if prevRootIndex == rootIndex {
   282  			result = append(result, part)
   283  		}
   284  	}
   285  
   286  	var frag string
   287  	for i := len(result) - 1; i >= 0; i-- {
   288  		frag = filepath.Join(frag, result[i])
   289  	}
   290  
   291  	return filepath.Join(root, frag)
   292  }
   293  
   294  // isPathRelative returns true if path starts with "./" or "../".
   295  // It is intended for use on import paths, where "/" is always the directory separator.
   296  func isPathRelative(s string) bool {
   297  	return strings.HasPrefix(s, "./") || strings.HasPrefix(s, "../")
   298  }