github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/gnovm/pkg/transpiler/transpiler.go (about) 1 package transpiler 2 3 import ( 4 "bytes" 5 "fmt" 6 "go/ast" 7 "go/format" 8 "go/parser" 9 goscanner "go/scanner" 10 "go/token" 11 "os" 12 "os/exec" 13 "path/filepath" 14 "regexp" 15 "sort" 16 "strconv" 17 "strings" 18 19 "golang.org/x/tools/go/ast/astutil" 20 ) 21 22 const ( 23 GnoRealmPkgsPrefixBefore = "gno.land/r/" 24 GnoRealmPkgsPrefixAfter = "github.com/gnolang/gno/examples/gno.land/r/" 25 GnoPackagePrefixBefore = "gno.land/p/demo/" 26 GnoPackagePrefixAfter = "github.com/gnolang/gno/examples/gno.land/p/demo/" 27 GnoStdPkgBefore = "std" 28 GnoStdPkgAfter = "github.com/gnolang/gno/gnovm/stdlibs/stdshim" 29 ) 30 31 var stdlibWhitelist = []string{ 32 // go 33 "bufio", 34 "bytes", 35 "compress/gzip", 36 "context", 37 "crypto/md5", 38 "crypto/sha1", 39 "crypto/chacha20", 40 "crypto/cipher", 41 "crypto/sha256", 42 "encoding/base64", 43 "encoding/binary", 44 "encoding/hex", 45 "encoding/json", 46 "encoding/xml", 47 "errors", 48 "hash", 49 "hash/adler32", 50 "internal/bytealg", 51 "internal/os", 52 "flag", 53 "fmt", 54 "io", 55 "io/util", 56 "math", 57 "math/big", 58 "math/bits", 59 "math/rand", 60 "net/url", 61 "path", 62 "regexp", 63 "sort", 64 "strconv", 65 "strings", 66 "text/template", 67 "time", 68 "unicode", 69 "unicode/utf8", 70 "unicode/utf16", 71 72 // gno 73 "std", 74 } 75 76 var importPrefixWhitelist = []string{ 77 "github.com/gnolang/gno/_test", 78 } 79 80 const ImportPrefix = "github.com/gnolang/gno" 81 82 type transpileResult struct { 83 Imports []*ast.ImportSpec 84 Translated string 85 } 86 87 // TODO: func TranspileFile: supports caching. 88 // TODO: func TranspilePkg: supports directories. 89 90 func guessRootDir(fileOrPkg string, goBinary string) (string, error) { 91 abs, err := filepath.Abs(fileOrPkg) 92 if err != nil { 93 return "", err 94 } 95 args := []string{"list", "-m", "-mod=mod", "-f", "{{.Dir}}", ImportPrefix} 96 cmd := exec.Command(goBinary, args...) 97 cmd.Dir = abs 98 out, err := cmd.CombinedOutput() 99 if err != nil { 100 return "", fmt.Errorf("can't guess --root-dir") 101 } 102 rootDir := strings.TrimSpace(string(out)) 103 return rootDir, nil 104 } 105 106 // GetTranspileFilenameAndTags returns the filename and tags for transpiled files. 107 func GetTranspileFilenameAndTags(gnoFilePath string) (targetFilename, tags string) { 108 nameNoExtension := strings.TrimSuffix(filepath.Base(gnoFilePath), ".gno") 109 switch { 110 case strings.HasSuffix(gnoFilePath, "_filetest.gno"): 111 tags = "gno && filetest" 112 targetFilename = "." + nameNoExtension + ".gno.gen.go" 113 case strings.HasSuffix(gnoFilePath, "_test.gno"): 114 tags = "gno && test" 115 targetFilename = "." + nameNoExtension + ".gno.gen_test.go" 116 default: 117 tags = "gno" 118 targetFilename = nameNoExtension + ".gno.gen.go" 119 } 120 return 121 } 122 123 func Transpile(source string, tags string, filename string) (*transpileResult, error) { 124 fset := token.NewFileSet() 125 f, err := parser.ParseFile(fset, filename, source, parser.ParseComments) 126 if err != nil { 127 return nil, fmt.Errorf("parse: %w", err) 128 } 129 130 isTestFile := strings.HasSuffix(filename, "_test.gno") || strings.HasSuffix(filename, "_filetest.gno") 131 shouldCheckWhitelist := !isTestFile 132 133 transformed, err := transpileAST(fset, f, shouldCheckWhitelist) 134 if err != nil { 135 return nil, fmt.Errorf("transpileAST: %w", err) 136 } 137 138 var out bytes.Buffer 139 // Write file header 140 out.WriteString("// Code generated by github.com/gnolang/gno. DO NOT EDIT.\n\n") 141 if tags != "" { 142 fmt.Fprintf(&out, "//go:build %s\n\n", tags) 143 } 144 // Add a //line directive so the go compiler outputs the original gno 145 // filename and the file's position that corresponds to it. 146 // See https://pkg.go.dev/cmd/compile#hdr-Compiler_Directives 147 fmt.Fprintf(&out, "//line %s:1:1\n", filepath.Base(filename)) 148 149 // Write file content and format it. 150 err = format.Node(&out, fset, transformed) 151 if err != nil { 152 return nil, fmt.Errorf("format.Node: %w", err) 153 } 154 155 res := &transpileResult{ 156 Imports: f.Imports, 157 Translated: out.String(), 158 } 159 return res, nil 160 } 161 162 // TranspileVerifyFile tries to run `go fmt` against a transpiled .go file. 163 // 164 // This is fast and won't look the imports. 165 func TranspileVerifyFile(path string, gofmtBinary string) error { 166 // TODO: use cmd/parser instead of exec? 167 168 args := strings.Split(gofmtBinary, " ") 169 args = append(args, []string{"-l", "-e", path}...) 170 cmd := exec.Command(args[0], args[1:]...) 171 out, err := cmd.CombinedOutput() 172 if err != nil { 173 fmt.Fprintln(os.Stderr, string(out)) 174 return fmt.Errorf("%s: %w", gofmtBinary, err) 175 } 176 return nil 177 } 178 179 // TranspileBuildPackage tries to run `go build` against the transpiled .go files. 180 // 181 // This method is the most efficient to detect errors but requires that 182 // all the import are valid and available. 183 func TranspileBuildPackage(fileOrPkg, goBinary string) error { 184 // TODO: use cmd/compile instead of exec? 185 // TODO: find the nearest go.mod file, chdir in the same folder, rim prefix? 186 // TODO: temporarily create an in-memory go.mod or disable go modules for gno? 187 // TODO: ignore .go files that were not generated from gno? 188 // TODO: automatically transpile if not yet done. 189 190 files := []string{} 191 192 info, err := os.Stat(fileOrPkg) 193 if err != nil { 194 return fmt.Errorf("invalid file or package path %s: %w", fileOrPkg, err) 195 } 196 if !info.IsDir() { 197 file := fileOrPkg 198 files = append(files, file) 199 } else { 200 pkgDir := fileOrPkg 201 goGlob := filepath.Join(pkgDir, "*.go") 202 goMatches, err := filepath.Glob(goGlob) 203 if err != nil { 204 return fmt.Errorf("glob %s: %w", goGlob, err) 205 } 206 for _, goMatch := range goMatches { 207 switch { 208 case strings.HasPrefix(goMatch, "."): // skip 209 case strings.HasSuffix(goMatch, "_filetest.go"): // skip 210 case strings.HasSuffix(goMatch, "_filetest.gno.gen.go"): // skip 211 case strings.HasSuffix(goMatch, "_test.go"): // skip 212 case strings.HasSuffix(goMatch, "_test.gno.gen.go"): // skip 213 default: 214 files = append(files, goMatch) 215 } 216 } 217 } 218 219 sort.Strings(files) 220 args := append([]string{"build", "-v", "-tags=gno"}, files...) 221 cmd := exec.Command(goBinary, args...) 222 rootDir, err := guessRootDir(fileOrPkg, goBinary) 223 if err == nil { 224 cmd.Dir = rootDir 225 } 226 out, err := cmd.CombinedOutput() 227 if _, ok := err.(*exec.ExitError); ok { 228 // exit error 229 return parseGoBuildErrors(string(out)) 230 } 231 return err 232 } 233 234 var errorRe = regexp.MustCompile(`(?m)^(\S+):(\d+):(\d+): (.+)$`) 235 236 // parseGoBuildErrors returns a scanner.ErrorList filled with all errors found 237 // in out, which is supposed to be the output of the `go build` command. 238 // 239 // TODO(tb): update when `go build -json` is released to replace regexp usage. 240 // See https://github.com/golang/go/issues/62067 241 func parseGoBuildErrors(out string) error { 242 var errList goscanner.ErrorList 243 matches := errorRe.FindAllStringSubmatch(out, -1) 244 for _, match := range matches { 245 filename := match[1] 246 line, err := strconv.Atoi(match[2]) 247 if err != nil { 248 return fmt.Errorf("parse line go build error %s: %w", match, err) 249 } 250 251 column, err := strconv.Atoi(match[3]) 252 if err != nil { 253 return fmt.Errorf("parse column go build error %s: %w", match, err) 254 } 255 msg := match[4] 256 errList.Add(token.Position{ 257 Filename: filename, 258 Line: line, 259 Column: column, 260 }, msg) 261 } 262 return errList.Err() 263 } 264 265 func transpileAST(fset *token.FileSet, f *ast.File, checkWhitelist bool) (ast.Node, error) { 266 var errs goscanner.ErrorList 267 268 imports := astutil.Imports(fset, f) 269 270 // import whitelist 271 if checkWhitelist { 272 for _, paragraph := range imports { 273 for _, importSpec := range paragraph { 274 importPath := strings.TrimPrefix(strings.TrimSuffix(importSpec.Path.Value, `"`), `"`) 275 276 if strings.HasPrefix(importPath, GnoRealmPkgsPrefixBefore) { 277 continue 278 } 279 280 if strings.HasPrefix(importPath, GnoPackagePrefixBefore) { 281 continue 282 } 283 284 valid := false 285 for _, whitelisted := range stdlibWhitelist { 286 if importPath == whitelisted { 287 valid = true 288 break 289 } 290 } 291 if valid { 292 continue 293 } 294 295 for _, whitelisted := range importPrefixWhitelist { 296 if strings.HasPrefix(importPath, whitelisted) { 297 valid = true 298 break 299 } 300 } 301 if valid { 302 continue 303 } 304 305 errs.Add(fset.Position(importSpec.Pos()), fmt.Sprintf("import %q is not in the whitelist", importPath)) 306 } 307 } 308 } 309 310 // rewrite imports 311 for _, paragraph := range imports { 312 for _, importSpec := range paragraph { 313 importPath := strings.TrimPrefix(strings.TrimSuffix(importSpec.Path.Value, `"`), `"`) 314 315 // std package 316 if importPath == GnoStdPkgBefore { 317 if !astutil.RewriteImport(fset, f, GnoStdPkgBefore, GnoStdPkgAfter) { 318 errs.Add(fset.Position(importSpec.Pos()), fmt.Sprintf("failed to replace the %q package with %q", GnoStdPkgBefore, GnoStdPkgAfter)) 319 } 320 } 321 322 // p/pkg packages 323 if strings.HasPrefix(importPath, GnoPackagePrefixBefore) { 324 target := GnoPackagePrefixAfter + strings.TrimPrefix(importPath, GnoPackagePrefixBefore) 325 326 if !astutil.RewriteImport(fset, f, importPath, target) { 327 errs.Add(fset.Position(importSpec.Pos()), fmt.Sprintf("failed to replace the %q package with %q", importPath, target)) 328 } 329 } 330 331 // r/realm packages 332 if strings.HasPrefix(importPath, GnoRealmPkgsPrefixBefore) { 333 target := GnoRealmPkgsPrefixAfter + strings.TrimPrefix(importPath, GnoRealmPkgsPrefixBefore) 334 335 if !astutil.RewriteImport(fset, f, importPath, target) { 336 errs.Add(fset.Position(importSpec.Pos()), fmt.Sprintf("failed to replace the %q package with %q", importPath, target)) 337 } 338 } 339 } 340 } 341 342 // custom handler 343 node := astutil.Apply(f, 344 // pre 345 func(c *astutil.Cursor) bool { 346 // do things here 347 return true 348 }, 349 // post 350 func(c *astutil.Cursor) bool { 351 // and here 352 return true 353 }, 354 ) 355 return node, errs.Err() 356 }