From a63ca1b377c5f3dbfe8af9c00aa8a89d6f512a2f Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Sun, 3 Sep 2023 09:56:52 +0200 Subject: [PATCH 1/4] feat: add gnoffee PoC Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com> --- gnovm/cmd/gnoffee/main.go | 133 +++++++++++++++ gnovm/cmd/gnoffee/main_test.go | 50 ++++++ .../testdata/valid_sample_with_export.txtar | 60 +++++++ gnovm/pkg/gnoffee/doc.go | 30 ++++ gnovm/pkg/gnoffee/gnoffee_test.go | 76 +++++++++ gnovm/pkg/gnoffee/stage1.go | 18 ++ gnovm/pkg/gnoffee/stage1_test.go | 104 ++++++++++++ gnovm/pkg/gnoffee/stage2.go | 159 ++++++++++++++++++ gnovm/pkg/gnoffee/stage2_test.go | 139 +++++++++++++++ gnovm/pkg/gnoffee/utils.go | 51 ++++++ gnovm/pkg/gnoffee/utils_test.go | 85 ++++++++++ 11 files changed, 905 insertions(+) create mode 100644 gnovm/cmd/gnoffee/main.go create mode 100644 gnovm/cmd/gnoffee/main_test.go create mode 100644 gnovm/cmd/gnoffee/testdata/valid_sample_with_export.txtar create mode 100644 gnovm/pkg/gnoffee/doc.go create mode 100644 gnovm/pkg/gnoffee/gnoffee_test.go create mode 100644 gnovm/pkg/gnoffee/stage1.go create mode 100644 gnovm/pkg/gnoffee/stage1_test.go create mode 100644 gnovm/pkg/gnoffee/stage2.go create mode 100644 gnovm/pkg/gnoffee/stage2_test.go create mode 100644 gnovm/pkg/gnoffee/utils.go create mode 100644 gnovm/pkg/gnoffee/utils_test.go diff --git a/gnovm/cmd/gnoffee/main.go b/gnovm/cmd/gnoffee/main.go new file mode 100644 index 00000000000..e6825c240e7 --- /dev/null +++ b/gnovm/cmd/gnoffee/main.go @@ -0,0 +1,133 @@ +package main + +import ( + "flag" + "fmt" + "go/ast" + "go/parser" + "go/printer" + "go/token" + "io/ioutil" + "os" + "path/filepath" + + "github.com/gnolang/gno/gnovm/pkg/gnoffee" +) + +var writeFlag bool + +func init() { + flag.BoolVar(&writeFlag, "w", false, "write result to gnoffee.gen.go file instead of stdout") +} + +func main() { + flag.Parse() + args := flag.Args() + + if len(args) < 1 { + fmt.Fprintln(os.Stderr, "Usage: gnoffee [-w] ") + return + } + + err := doMain(args[0]) + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } +} + +func doMain(arg string) error { + fset, pkg, err := processPackageOrFileOrStdin(arg) + if err != nil { + return fmt.Errorf("parse error: %w", err) + } + + newFile, err := gnoffee.Stage2(pkg) + if err != nil { + return fmt.Errorf("processing the AST: %w", err) + } + + // combine existing files into newFile to generate a unique file for the whole package. + for _, file := range pkg { + newFile.Decls = append(newFile.Decls, file.Decls...) + } + + if writeFlag { + filename := "gnoffee.gen.go" + f, err := os.Create(filename) + if err != nil { + return fmt.Errorf("creating file %q: %w", filename, err) + } + defer f.Close() + + err = printer.Fprint(f, fset, newFile) + if err != nil { + return fmt.Errorf("writing to file %q: %w", filename, err) + } + } else { + _ = printer.Fprint(os.Stdout, fset, newFile) + } + return nil +} + +func processPackageOrFileOrStdin(arg string) (*token.FileSet, map[string]*ast.File, error) { + var fset = token.NewFileSet() + var pkg = map[string]*ast.File{} + + processFile := func(data []byte, filename string) error { + source := string(data) + source = gnoffee.Stage1(source) + + parsedFile, err := parser.ParseFile(fset, filename, source, parser.ParseComments) + if err != nil { + return fmt.Errorf("parsing file %q: %v", filename, err) + } + pkg[filename] = parsedFile + return nil + } + + // process arg + if arg == "-" { + // Read from stdin and process + data, err := ioutil.ReadAll(os.Stdin) + if err != nil { + return nil, nil, fmt.Errorf("reading from stdin: %w", err) + } + if err := processFile(data, "stdin.gnoffee"); err != nil { + return nil, nil, err + } + } else { + // If it's a directory, gather all .go and .gnoffee files and process accordingly + if info, err := os.Stat(arg); err == nil && info.IsDir() { + err := filepath.Walk(arg, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + ext := filepath.Ext(path) + if ext == ".gnoffee" { + data, err := ioutil.ReadFile(path) + if err != nil { + return fmt.Errorf("reading file %q: %v", path, err) + } + if err := processFile(data, path); err != nil { + return err + } + } + return nil + }) + if err != nil { + return nil, nil, err + } + } else { + data, err := ioutil.ReadFile(arg) + if err != nil { + return nil, nil, fmt.Errorf("reading file %q: %w", arg, err) + } + if err := processFile(data, arg); err != nil { + return nil, nil, err + } + } + } + return fset, pkg, nil +} diff --git a/gnovm/cmd/gnoffee/main_test.go b/gnovm/cmd/gnoffee/main_test.go new file mode 100644 index 00000000000..5cee00d1530 --- /dev/null +++ b/gnovm/cmd/gnoffee/main_test.go @@ -0,0 +1,50 @@ +package main + +import ( + "os/exec" + "path/filepath" + "testing" + + "github.com/jaekwon/testify/require" + "github.com/rogpeppe/go-internal/testscript" +) + +func TestTest(t *testing.T) { + testscript.Run(t, setupTestScript(t, "testdata")) +} + +func setupTestScript(t *testing.T, txtarDir string) testscript.Params { + t.Helper() + // Get root location of github.com/gnolang/gno + goModPath, err := exec.Command("go", "env", "GOMOD").CombinedOutput() + require.NoError(t, err) + rootDir := filepath.Dir(string(goModPath)) + // Build a fresh gno binary in a temp directory + gnoffeeBin := filepath.Join(t.TempDir(), "gnoffee") + err = exec.Command("go", "build", "-o", gnoffeeBin, filepath.Join(rootDir, "gnovm", "cmd", "gnoffee")).Run() + require.NoError(t, err) + // Define script params + return testscript.Params{ + Setup: func(env *testscript.Env) error { + return nil + }, + Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){ + // add a custom "gnoffee" command so txtar files can easily execute "gno" + // without knowing where is the binary or how it is executed. + "gnoffee": func(ts *testscript.TestScript, neg bool, args []string) { + err := ts.Exec(gnoffeeBin, args...) + if err != nil { + ts.Logf("[%v]\n", err) + if !neg { + ts.Fatalf("unexpected gnoffee command failure") + } + } else { + if neg { + ts.Fatalf("unexpected gnoffee command success") + } + } + }, + }, + Dir: txtarDir, + } +} diff --git a/gnovm/cmd/gnoffee/testdata/valid_sample_with_export.txtar b/gnovm/cmd/gnoffee/testdata/valid_sample_with_export.txtar new file mode 100644 index 00000000000..0b1163c46ef --- /dev/null +++ b/gnovm/cmd/gnoffee/testdata/valid_sample_with_export.txtar @@ -0,0 +1,60 @@ +# Test with a valid sample.gnoffee + +gnoffee -w . + +! stderr .+ +! stdout .+ + +cmp gen.golden gnoffee.gen.go + +-- sample.gnoffee -- +package sample + +type foo struct{} + +export baz as Bar + +var baz = foo{} + +func (f *foo) Hello() string { + return "Hello from foo!" +} + +func (f *foo) Bye() { + println("Goodbye from foo!") +} + +type Bar interface { + Hello() string + Bye() +} + +-- gen.golden -- +package sample + +// This function was generated by gnoffee due to the export directive. +func Hello() string { + return baz.Hello() +} + +// This function was generated by gnoffee due to the export directive. +func Bye() { + baz.Bye() +} + +type foo struct{} + +var baz = foo{} + +func (f *foo) Hello() string { + return "Hello from foo!" +} + +func (f *foo) Bye() { + println("Goodbye from foo!") +} + +type Bar interface { + Hello() string + Bye() +} diff --git a/gnovm/pkg/gnoffee/doc.go b/gnovm/pkg/gnoffee/doc.go new file mode 100644 index 00000000000..f2c17cbace5 --- /dev/null +++ b/gnovm/pkg/gnoffee/doc.go @@ -0,0 +1,30 @@ +// Package gnoffee provides a transpiler that extends the Go language +// with additional, custom keywords. These keywords offer enhanced +// functionality, aiming to make Go programming even more efficient +// and expressive. +// +// Current supported keywords and transformations: +// - `export as `: +// This allows for the automatic generation of top-level functions +// in the package that call methods on a specific instance of the struct. +// It's a way to "expose" or "proxy" methods of a struct via free functions. +// +// How Gnoffee Works: +// Gnoffee operates in multiple stages. The first stage transforms +// gnoffee-specific keywords into their comment directive equivalents, +// paving the way for the second stage to handle the transpiling logic. +// +// The Package Path: +// Gnoffee is currently housed under the gnovm namespace, with the +// package path being: github.com/gnolang/gno/gnovm/pkg/gnoffee. +// +// However, it's important to note that while gnoffee resides in the gnovm +// namespace, it operates independently from the gnovm. There's potential +// for gnoffee to be relocated in the future based on its evolving role +// and development trajectory. +// +// Future Changes: +// As the Go and Gno ecosystems and requirements evolve, gnoffee might see the +// introduction of new keywords or alterations to its current functionality. +// Always refer to the package documentation for the most up-to-date details. +package gnoffee diff --git a/gnovm/pkg/gnoffee/gnoffee_test.go b/gnovm/pkg/gnoffee/gnoffee_test.go new file mode 100644 index 00000000000..57ae3aaa580 --- /dev/null +++ b/gnovm/pkg/gnoffee/gnoffee_test.go @@ -0,0 +1,76 @@ +package gnoffee + +import ( + "bytes" + "go/ast" + "go/format" + "go/parser" + "go/token" + "testing" +) + +func TestPackage(t *testing.T) { + inputCode := ` +package sample + +export foo as Bar + +type foo struct{} + +func (f *foo) Hello() string { + return "Hello from foo!" +} + +func (f *foo) Bye() { + println("Goodbye from foo!") +} + +type Bar interface { + Hello() string + Bye() +} +` + expectedOutput := ` +package sample + +// This function was generated by gnoffee due to the export directive. +func Hello() string { + return foo.Hello() +} + +// This function was generated by gnoffee due to the export directive. +func Bye() { + foo.Bye() +} +` + + // Stage 1 + inputCode = Stage1(inputCode) + + // Stage 2 + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "sample.go", inputCode, parser.ParseComments) + if err != nil { + t.Fatalf("Failed to parse input: %v", err) + } + + files := map[string]*ast.File{ + "sample.go": file, + } + + generatedFile, err := Stage2(files) + if err != nil { + t.Fatalf("Error during Stage2 generation: %v", err) + } + + var buf bytes.Buffer + if err := format.Node(&buf, fset, generatedFile); err != nil { + t.Fatalf("Failed to format generated output: %v", err) + } + + generatedCode := normalizeGoCode(buf.String()) + expected := normalizeGoCode(expectedOutput) + if generatedCode != expected { + t.Errorf("Generated code does not match expected output.\nExpected:\n\n%v\n\nGot:\n\n%v", expected, generatedCode) + } +} diff --git a/gnovm/pkg/gnoffee/stage1.go b/gnovm/pkg/gnoffee/stage1.go new file mode 100644 index 00000000000..07b3437eca3 --- /dev/null +++ b/gnovm/pkg/gnoffee/stage1.go @@ -0,0 +1,18 @@ +package gnoffee + +import ( + "regexp" +) + +// Stage1 converts the gnoffee-specific keywords into their comment directive equivalents. +func Stage1(src string) string { + // Handling the 'export' keyword + exportRegex := regexp.MustCompile(`(?m)^export\s+`) + src = exportRegex.ReplaceAllString(src, "//gnoffee:export ") + + // Handling the 'invar' keyword + invarRegex := regexp.MustCompile(`(?m)^invar\s+([\w\d_]+)\s+(.+)`) + src = invarRegex.ReplaceAllString(src, "//gnoffee:invar $1\nvar $1 $2") + + return src +} diff --git a/gnovm/pkg/gnoffee/stage1_test.go b/gnovm/pkg/gnoffee/stage1_test.go new file mode 100644 index 00000000000..b05c24455a1 --- /dev/null +++ b/gnovm/pkg/gnoffee/stage1_test.go @@ -0,0 +1,104 @@ +package gnoffee + +import ( + "testing" +) + +func TestStage1(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "Basic Export Functionality", + input: ` +export Foo as FooInstance +invar BarInterface = Baz +`, + expected: ` +//gnoffee:export Foo as FooInstance +//gnoffee:invar BarInterface +var BarInterface = Baz +`, + }, + { + name: "Complex Input with Mixed Code", + input: ` +func someFunction() { + println("Hello, World!") +} + +export Baz as BazInstance +invar QuxInterface = Baz + +func anotherFunction() bool { + return true +} + +export Quux as QuuxInstance +`, + expected: ` +func someFunction() { + println("Hello, World!") +} + +//gnoffee:export Baz as BazInstance +//gnoffee:invar QuxInterface +var QuxInterface = Baz + +func anotherFunction() bool { + return true +} + +//gnoffee:export Quux as QuuxInstance +`, + }, + { + name: "Input with No Changes", + input: ` +func simpleFunction() { + println("Just a simple function!") +} +`, + expected: ` +func simpleFunction() { + println("Just a simple function!") +} +`, + }, + { + name: "Already Annotated Source", + input: ` +// Some comment +//gnoffee:export AlreadyExported as AlreadyInstance +func someFunction() { + println("This function is already annotated!") +} + +//gnoffee:invar AlreadyInterface +var AlreadyInterface Already +`, + expected: ` +// Some comment +//gnoffee:export AlreadyExported as AlreadyInstance +func someFunction() { + println("This function is already annotated!") +} + +//gnoffee:invar AlreadyInterface +var AlreadyInterface Already +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + output := Stage1(tt.input) + + if output != tt.expected { + t.Errorf("Expected:\n%s\nGot:\n%s\n", tt.expected, output) + } + }) + } +} diff --git a/gnovm/pkg/gnoffee/stage2.go b/gnovm/pkg/gnoffee/stage2.go new file mode 100644 index 00000000000..20db56d0b4a --- /dev/null +++ b/gnovm/pkg/gnoffee/stage2.go @@ -0,0 +1,159 @@ +package gnoffee + +import ( + "errors" + "fmt" + "go/ast" + "strings" +) + +// Stage2 transforms the given AST files based on gnoffee directives +// and returns an AST for a new generated file based on the provided files. +func Stage2(files map[string]*ast.File) (*ast.File, error) { + return generateFile(files) +} + +func generateFile(pkg map[string]*ast.File) (*ast.File, error) { + exportMapping := make(map[string]string) + var packageName string + + for _, f := range pkg { + if packageName == "" && f.Name != nil { + packageName = f.Name.Name + } + + // Iterate over all comments in the file. + for _, commentGroup := range f.Comments { + for _, comment := range commentGroup.List { + // Make sure the comment starts a new line. + if strings.HasPrefix(comment.Text, "//") { + parts := strings.Fields(comment.Text) + switch parts[0] { + case "//gnoffee:export": + if len(parts) == 4 && parts[2] == "as" { + k, v := parts[1], parts[3] + exportMapping[k] = v + } else { + return nil, errors.New("invalid gnoffee:export syntax") + } + case "//gnoffee:invar": + return nil, errors.New("unimplemented: invar keyword") + default: + return nil, fmt.Errorf("unknown gnoffee keyword: %s", parts[0]) + } + } + } + } + } + + newFile := &ast.File{ + Name: &ast.Ident{Name: packageName}, + Decls: make([]ast.Decl, 0), + } + + // Now, populate the newFile with the necessary declarations based on the exportMapping. + for k, v := range exportMapping { + for _, f := range pkg { + for _, decl := range f.Decls { + + genDecl, ok := decl.(*ast.GenDecl) + if !ok { + continue + } + + for _, spec := range genDecl.Specs { + typeSpec, ok := spec.(*ast.TypeSpec) + if !ok { + continue + } + + iface, ok := typeSpec.Type.(*ast.InterfaceType) + if !ok { + continue + } + + if typeSpec.Name.Name != v { + continue + } + + for _, method := range iface.Methods.List { + fnDecl := &ast.FuncDecl{ + Name: method.Names[0], + Doc: &ast.CommentGroup{ + List: []*ast.Comment{ + { + Text: "\n// This function was generated by gnoffee due to the export directive.", + }, + }, + }, + Type: method.Type.(*ast.FuncType), + Body: &ast.BlockStmt{ + List: make([]ast.Stmt, 0), + }, + } + + callExpr := &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: ast.NewIdent(k), + Sel: method.Names[0], + }, + Args: funcTypeToIdentList(method.Type.(*ast.FuncType).Params), + } + + // Check if the method has return values + if method.Type.(*ast.FuncType).Results != nil && len(method.Type.(*ast.FuncType).Results.List) > 0 { + retStmt := &ast.ReturnStmt{ + Results: []ast.Expr{callExpr}, + } + fnDecl.Body.List = append(fnDecl.Body.List, retStmt) + } else { + exprStmt := &ast.ExprStmt{X: callExpr} + fnDecl.Body.List = append(fnDecl.Body.List, exprStmt) + } + + newFile.Decls = append(newFile.Decls, fnDecl) + } + } + } + } + } + + return newFile, nil +} + +func funcTypeToIdentList(fields *ast.FieldList) []ast.Expr { + var idents []ast.Expr + for _, field := range fields.List { + for _, name := range field.Names { + idents = append(idents, ast.NewIdent(name.Name)) + } + } + return idents +} + +func findObjectByName(file *ast.File, objectName string) ast.Node { + for _, decl := range file.Decls { + switch d := decl.(type) { + case *ast.GenDecl: + for _, spec := range d.Specs { + switch s := spec.(type) { + case *ast.TypeSpec: + if s.Name.Name == objectName { + return s.Type + } + case *ast.ValueSpec: + for _, name := range s.Names { + if name.Name == objectName { + return s.Type + } + } + } + } + case *ast.FuncDecl: + if d.Name.Name == objectName { + return d.Type + } + } + } + return nil +} diff --git a/gnovm/pkg/gnoffee/stage2_test.go b/gnovm/pkg/gnoffee/stage2_test.go new file mode 100644 index 00000000000..cc20d64097e --- /dev/null +++ b/gnovm/pkg/gnoffee/stage2_test.go @@ -0,0 +1,139 @@ +package gnoffee + +import ( + "bytes" + "go/ast" + "go/format" + "go/parser" + "go/token" + "testing" +) + +func TestStage2(t *testing.T) { + tests := []struct { + name string + input string + wantOutput string + wantErr bool + }{ + { + name: "Basic Test", + input: ` + package test + + //gnoffee:export baz as Helloer + + type Helloer interface { + Hello() string + } + + type foo struct{} + + func (f *foo) Hello() string { + return "Hello from foo!" + } + + func (f *foo) Bye() { } + + var baz = foo{} + + var _ Helloer = &foo{} + `, + wantOutput: ` + package test + + // This function was generated by gnoffee due to the export directive. + func Hello() string { + return baz.Hello() + } + `, + wantErr: false, + }, + { + name: "Invalid Export Syntax", + input: ` + package test + + var foo struct{} + //gnoffee:export foo MyInterface3 + type MyInterface3 interface { + Baz() + } + `, + wantErr: true, + }, + { + name: "Already Annotated With gnoffee Comment", + input: ` + package test + + var foo = struct{} + + //gnoffee:export foo as MyInterface4 + type MyInterface4 interface { + Qux() + } + `, + wantOutput: ` + package test + + // This function was generated by gnoffee due to the export directive. + func Qux() { + foo.Qux() + } + `, + wantErr: false, + }, + { + name: "No Export Directive", + input: ` + package test + + type SimpleInterface interface { + Moo() + } + `, + wantOutput: ` + package test + `, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "", tt.input, parser.ParseComments) + if err != nil { + t.Fatalf("Failed to parse input: %v", err) + } + + files := map[string]*ast.File{ + "test.go": file, + } + + generatedFile, err := Stage2(files) + switch { + case err == nil && tt.wantErr: + t.Fatalf("Expected an error") + case err != nil && !tt.wantErr: + t.Fatalf("Error during Stage2 generation: %v", err) + case err != nil && tt.wantErr: + return + case err == nil && !tt.wantErr: + // noop + } + + var buf bytes.Buffer + if err := format.Node(&buf, fset, generatedFile); err != nil { + t.Fatalf("Failed to format generated output: %v", err) + } + + generatedCode := normalizeGoCode(buf.String()) + expected := normalizeGoCode(tt.wantOutput) + if generatedCode != expected { + t.Errorf("Transformed code does not match expected output.\nExpected:\n\n%v\n\nGot:\n\n%v", expected, generatedCode) + } + }) + } +} diff --git a/gnovm/pkg/gnoffee/utils.go b/gnovm/pkg/gnoffee/utils.go new file mode 100644 index 00000000000..5d49403e89a --- /dev/null +++ b/gnovm/pkg/gnoffee/utils.go @@ -0,0 +1,51 @@ +package gnoffee + +import ( + "strings" +) + +// normalizeGoCode normalizes a multi-line Go code string by +// trimming the common leading white spaces from each line while preserving indentation. +func normalizeGoCode(code string) string { + code = strings.ReplaceAll(code, "\t", " ") + + lines := strings.Split(code, "\n") + + const defaultMax = 1337 // Initialize max with an arbitrary value + + // Determine the minimum leading whitespace across all lines + var minLeadingSpaces = defaultMax + for _, line := range lines { + // skip empty lines + if len(strings.TrimSpace(line)) == 0 { + continue + } + + leadingSpaces := len(line) - len(strings.TrimLeft(line, " ")) + // println(len(line), len(strings.TrimLeft(line, " ")), "AAA", strings.TrimLeft(line, " "), "BBB") + if leadingSpaces < minLeadingSpaces { + minLeadingSpaces = leadingSpaces + } + } + // println(minLeadingSpaces) + // println() + + if minLeadingSpaces == defaultMax { + return code + } + + // Trim the determined number of leading whitespaces from all lines + var normalizedLines []string + for _, line := range lines { + if len(line) > minLeadingSpaces { + normalizedLines = append(normalizedLines, line[minLeadingSpaces:]) + } else { + normalizedLines = append(normalizedLines, strings.TrimSpace(line)) + } + } + + normalizedCode := strings.Join(normalizedLines, "\n") + normalizedCode = strings.ReplaceAll(normalizedCode, " ", "\t") + normalizedCode = strings.TrimSpace(normalizedCode) + return normalizedCode +} diff --git a/gnovm/pkg/gnoffee/utils_test.go b/gnovm/pkg/gnoffee/utils_test.go new file mode 100644 index 00000000000..f090c49512f --- /dev/null +++ b/gnovm/pkg/gnoffee/utils_test.go @@ -0,0 +1,85 @@ +package gnoffee + +import ( + "testing" +) + +func TestNormalizeGoCode(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "Basic normalization", + input: ` + func main() { + println("Hello, World!") + } + `, + expected: `func main() { + println("Hello, World!") +}`, + }, + { + name: "No indentation", + input: `func main() { +println("Hello, World!") +}`, + expected: `func main() { +println("Hello, World!") +}`, + }, + { + name: "Mixed indentation 1", + input: ` + func main() { + println("Hello, World!") + }`, + expected: `func main() { + println("Hello, World!") +}`, + }, + { + name: "Mixed indentation 2", + input: ` + func main() { + println("Hello, World!") + }`, + expected: `func main() { + println("Hello, World!") + }`, + }, + { + name: "Only one line with spaces", + input: " single line with spaces", + expected: "single line with spaces", + }, + { + name: "Empty lines", + input: ` + + func main() { + + println("Hello!") + + } + + `, + expected: `func main() { + + println("Hello!") + +}`, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + normalized := normalizeGoCode(test.input) + if normalized != test.expected { + t.Errorf("Expected:\n%s\nGot:\n%s", test.expected, normalized) + } + }) + } +} From 690ec95a40e9ec0e92cf655d2ce54a7b125e6e59 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Mon, 4 Sep 2023 01:47:53 +0200 Subject: [PATCH 2/4] chore: fixup Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com> --- gnovm/cmd/gnoffee/main.go | 4 ++-- gnovm/pkg/gnoffee/utils.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gnovm/cmd/gnoffee/main.go b/gnovm/cmd/gnoffee/main.go index e6825c240e7..3d738da7f7d 100644 --- a/gnovm/cmd/gnoffee/main.go +++ b/gnovm/cmd/gnoffee/main.go @@ -71,8 +71,8 @@ func doMain(arg string) error { } func processPackageOrFileOrStdin(arg string) (*token.FileSet, map[string]*ast.File, error) { - var fset = token.NewFileSet() - var pkg = map[string]*ast.File{} + fset := token.NewFileSet() + pkg := map[string]*ast.File{} processFile := func(data []byte, filename string) error { source := string(data) diff --git a/gnovm/pkg/gnoffee/utils.go b/gnovm/pkg/gnoffee/utils.go index 5d49403e89a..9675ad2a541 100644 --- a/gnovm/pkg/gnoffee/utils.go +++ b/gnovm/pkg/gnoffee/utils.go @@ -14,7 +14,7 @@ func normalizeGoCode(code string) string { const defaultMax = 1337 // Initialize max with an arbitrary value // Determine the minimum leading whitespace across all lines - var minLeadingSpaces = defaultMax + minLeadingSpaces := defaultMax for _, line := range lines { // skip empty lines if len(strings.TrimSpace(line)) == 0 { From 8dded81d39a01b11711f7467ea8b60a46059bc5f Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Mon, 4 Sep 2023 01:49:28 +0200 Subject: [PATCH 3/4] chore: fixup Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com> --- gnovm/cmd/gnoffee/main.go | 4 ++-- gnovm/pkg/gnoffee/stage2.go | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/gnovm/cmd/gnoffee/main.go b/gnovm/cmd/gnoffee/main.go index 3d738da7f7d..9f97e16f38f 100644 --- a/gnovm/cmd/gnoffee/main.go +++ b/gnovm/cmd/gnoffee/main.go @@ -80,7 +80,7 @@ func processPackageOrFileOrStdin(arg string) (*token.FileSet, map[string]*ast.Fi parsedFile, err := parser.ParseFile(fset, filename, source, parser.ParseComments) if err != nil { - return fmt.Errorf("parsing file %q: %v", filename, err) + return fmt.Errorf("parsing file %q: %w", filename, err) } pkg[filename] = parsedFile return nil @@ -108,7 +108,7 @@ func processPackageOrFileOrStdin(arg string) (*token.FileSet, map[string]*ast.Fi if ext == ".gnoffee" { data, err := ioutil.ReadFile(path) if err != nil { - return fmt.Errorf("reading file %q: %v", path, err) + return fmt.Errorf("reading file %q: %w", path, err) } if err := processFile(data, path); err != nil { return err diff --git a/gnovm/pkg/gnoffee/stage2.go b/gnovm/pkg/gnoffee/stage2.go index 20db56d0b4a..d26cbebbc14 100644 --- a/gnovm/pkg/gnoffee/stage2.go +++ b/gnovm/pkg/gnoffee/stage2.go @@ -55,7 +55,6 @@ func generateFile(pkg map[string]*ast.File) (*ast.File, error) { for k, v := range exportMapping { for _, f := range pkg { for _, decl := range f.Decls { - genDecl, ok := decl.(*ast.GenDecl) if !ok { continue From 29fb5b06f45321f0af773767dabe1a558f5bc689 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Mon, 4 Sep 2023 02:51:20 +0200 Subject: [PATCH 4/4] chore: fixup Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com> --- gnovm/cmd/gnoffee/main.go | 8 ++++++++ gnovm/cmd/gnoffee/testdata/valid_sample_with_export.txtar | 1 + gnovm/pkg/gnoffee/stage2.go | 4 +++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/gnovm/cmd/gnoffee/main.go b/gnovm/cmd/gnoffee/main.go index 9f97e16f38f..29a03a6187e 100644 --- a/gnovm/cmd/gnoffee/main.go +++ b/gnovm/cmd/gnoffee/main.go @@ -52,6 +52,9 @@ func doMain(arg string) error { newFile.Decls = append(newFile.Decls, file.Decls...) } + // Create a new package comment. + commentText := "// Code generated by \"gnoffee\". DO NOT EDIT." + if writeFlag { filename := "gnoffee.gen.go" f, err := os.Create(filename) @@ -60,11 +63,16 @@ func doMain(arg string) error { } defer f.Close() + _, err = fmt.Fprintln(f, commentText) + if err != nil { + return fmt.Errorf("writing to file %q: %w", filename, err) + } err = printer.Fprint(f, fset, newFile) if err != nil { return fmt.Errorf("writing to file %q: %w", filename, err) } } else { + _, _ = fmt.Println(commentText) _ = printer.Fprint(os.Stdout, fset, newFile) } return nil diff --git a/gnovm/cmd/gnoffee/testdata/valid_sample_with_export.txtar b/gnovm/cmd/gnoffee/testdata/valid_sample_with_export.txtar index 0b1163c46ef..5d07e7e31e6 100644 --- a/gnovm/cmd/gnoffee/testdata/valid_sample_with_export.txtar +++ b/gnovm/cmd/gnoffee/testdata/valid_sample_with_export.txtar @@ -30,6 +30,7 @@ type Bar interface { } -- gen.golden -- +// Code generated by "gnoffee". DO NOT EDIT. package sample // This function was generated by gnoffee due to the export directive. diff --git a/gnovm/pkg/gnoffee/stage2.go b/gnovm/pkg/gnoffee/stage2.go index d26cbebbc14..ed8bbbc7245 100644 --- a/gnovm/pkg/gnoffee/stage2.go +++ b/gnovm/pkg/gnoffee/stage2.go @@ -39,7 +39,9 @@ func generateFile(pkg map[string]*ast.File) (*ast.File, error) { case "//gnoffee:invar": return nil, errors.New("unimplemented: invar keyword") default: - return nil, fmt.Errorf("unknown gnoffee keyword: %s", parts[0]) + if strings.HasPrefix(parts[0], "//gnoffee:") { + return nil, fmt.Errorf("unknown gnoffee keyword: %s", parts[0]) + } } } }