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 }