diff --git a/cmd/protoc-gen-go-tableau-checker/ast.go b/cmd/protoc-gen-go-tableau-checker/ast.go new file mode 100644 index 0000000..f10b015 --- /dev/null +++ b/cmd/protoc-gen-go-tableau-checker/ast.go @@ -0,0 +1,38 @@ +package main + +import "go/ast" + +type ASTKey struct { + TypeName string + FuncName string +} + +func parseAST(file *ast.File) map[ASTKey]bool { + astMap := map[ASTKey]bool{} + for _, decl := range file.Decls { + // check function declarations + fn, ok := decl.(*ast.FuncDecl) + if !ok || fn.Recv == nil { + continue + } + // check receiver typename + if len(fn.Recv.List) != 1 { + continue + } + // must be pointer receiver + recvType, ok := fn.Recv.List[0].Type.(*ast.StarExpr) + if !ok { + continue + } + ident, ok := recvType.X.(*ast.Ident) + if !ok { + continue + } + // mark function as exists + astMap[ASTKey{ + TypeName: ident.Name, + FuncName: fn.Name.Name, + }] = true + } + return astMap +} diff --git a/cmd/protoc-gen-go-tableau-checker/check.go b/cmd/protoc-gen-go-tableau-checker/check.go index 2444299..8da2f47 100644 --- a/cmd/protoc-gen-go-tableau-checker/check.go +++ b/cmd/protoc-gen-go-tableau-checker/check.go @@ -1,11 +1,10 @@ package main import ( - "bufio" + "go/parser" + "go/token" "os" "path/filepath" - "regexp" - "strings" "github.com/tableauio/tableau/proto/tableaupb" "google.golang.org/protobuf/compiler/protogen" @@ -13,164 +12,93 @@ import ( "google.golang.org/protobuf/types/descriptorpb" ) -// const ( -// errorsPackage = protogen.GoImportPath("errors") -// fmtPackage = protogen.GoImportPath("fmt") -// formatPackage = protogen.GoImportPath("github.com/tableauio/tableau/format") -// loadPackage = protogen.GoImportPath("github.com/tableauio/tableau/load") -// ) - -// golbal container for record all proto filenames and messager names -var loaderImportPath protogen.GoImportPath - // generateMessager generates a protoconf file correponsing to the protobuf file. // Each wrapped struct type implement the Messager interface. -func generateMessager(gen *protogen.Plugin, file *protogen.File) { - loaderImportPath = protogen.GoImportPath(string(file.GoImportPath) + "/" + params.loaderPkg) - - filename := filepath.Join(file.GeneratedFilenamePrefix + "." + checkExt + ".go") - path := filepath.Join(params.outdir, filename) - existed, err := Exists(path) - if err != nil { - panic(err) - } - if existed { - g := gen.NewGeneratedFile(filename, "") - generateFileHeader(gen, file, g, false) - addIncrementalFileContent(gen, file, g, path) - } else { +func generateMessager(gen *protogen.Plugin) { + for _, f := range gen.Files { + if !NeedGenFile(f) { + continue + } + filename := filepath.Join(f.GeneratedFilenamePrefix + "." + checkExt + ".go") + path := filepath.Join(params.outdir, filename) + existed, err := Exists(path) + if err != nil { + panic(err) + } g := gen.NewGeneratedFile(filename, "") - generateFileHeader(gen, file, g, false) - g.P() - g.P("package ", params.pkg) - g.P() - generateFileContent(gen, file, g) + if existed { + addIncrementalFileContent(f, g, path) + } else { + generateFileHeader(f, g) + g.P() + g.P("package ", params.pkg) + g.P("import (") + g.P("tableau ", loaderImportPath) + g.P(")") + g.P() + generateFileContent(f, g) + } } } -var checkerRegexp *regexp.Regexp - -func init() { - checkerRegexp = regexp.MustCompile(`^type (.+) struct {`) // e.g.: type ItemConf struct { -} - -func addIncrementalFileContent(gen *protogen.Plugin, file *protogen.File, g *protogen.GeneratedFile, path string) { - f, err := os.Open(path) +func addIncrementalFileContent(file *protogen.File, g *protogen.GeneratedFile, path string) { + content, err := os.ReadFile(path) if err != nil { panic(err) } - defer f.Close() - - messagerMap := map[string]bool{} - var fileMessagers []string + g.P(string(content)) + fset := token.NewFileSet() + ast, err := parser.ParseFile(fset, path, content, 0) + if err != nil { + panic(err) + } + astMap := parseAST(ast) for _, message := range file.Messages { opts := message.Desc.Options().(*descriptorpb.MessageOptions) worksheet := proto.GetExtension(opts, tableaupb.E_Worksheet).(*tableaupb.WorksheetOptions) if worksheet != nil { messagerName := string(message.Desc.Name()) - messagerMap[messagerName] = false - fileMessagers = append(fileMessagers, messagerName) - } - } - content := "" - scanner := bufio.NewScanner(f) - line := 0 - headingCommentLines := 0 - initFirstLine := -1 - initEndLine := -1 - for scanner.Scan() { - line++ - if headingCommentLines+1 == line && strings.HasPrefix(scanner.Text(), "//") { - headingCommentLines++ - } - if line > headingCommentLines { - if strings.HasPrefix(scanner.Text(), "func init()") { - initFirstLine = line - } - if initFirstLine > 0 && initEndLine < 0 { - if strings.HasPrefix(scanner.Text(), "}") { - initEndLine = line - } - } - if initFirstLine < 0 || (initEndLine > 0 && line > initEndLine) { - content += scanner.Text() + "\n" - } - } - if matches := checkerRegexp.FindStringSubmatch(scanner.Text()); len(matches) > 0 { - msger := strings.TrimSpace(matches[1]) - if _, ok := messagerMap[msger]; ok { - messagerMap[msger] = true - } - } - } - if err := scanner.Err(); err != nil { - panic(err) - } + if _, ok := astMap[ASTKey{ + TypeName: messagerName, + FuncName: "Check", + }]; !ok { + generateCheck(g, messagerName) + } - g.P(content) - for messagerName, existed := range messagerMap { - if !existed { - genMessage(gen, file, g, messagerName, true) + if _, ok := astMap[ASTKey{ + TypeName: messagerName, + FuncName: "CheckCompatibility", + }]; !ok { + generateCheckCompatibility(g, messagerName) + } } } - - generateRegister(fileMessagers, g, true) } // generateFileContent generates struct type definitions. -func generateFileContent(gen *protogen.Plugin, file *protogen.File, g *protogen.GeneratedFile) { - var fileMessagers []string +func generateFileContent(file *protogen.File, g *protogen.GeneratedFile) { for _, message := range file.Messages { opts := message.Desc.Options().(*descriptorpb.MessageOptions) worksheet := proto.GetExtension(opts, tableaupb.E_Worksheet).(*tableaupb.WorksheetOptions) if worksheet != nil { messagerName := string(message.Desc.Name()) - genMessage(gen, file, g, messagerName, false) - fileMessagers = append(fileMessagers, messagerName) + generateCheck(g, messagerName) + generateCheckCompatibility(g, messagerName) } } - generateRegister(fileMessagers, g, false) } -func generateRegister(messagers []string, g *protogen.GeneratedFile, incremental bool) { - // register messagers - g.P("func init() {") - g.P("// NOTE: This func is auto-generated. DO NOT EDIT.") - for _, messager := range messagers { - g.P("register(func() checker {") - g.P("return new(", messager, ")") - g.P("})") - } - g.P("}") -} - -// genMessage generates a message definition. -func genMessage(gen *protogen.Plugin, file *protogen.File, g *protogen.GeneratedFile, messagerName string, incremental bool) { - // messager definition - g.P("type ", messagerName, " struct {") - if incremental { - g.P(params.loaderPkg, ".", messagerName) - } else { - g.P(loaderImportPath.Ident(messagerName)) - } - g.P("}") - g.P() - - var hubTypeIdent any - if incremental { - hubTypeIdent = params.loaderPkg + ".Hub" - } else { - hubTypeIdent = loaderImportPath.Ident("Hub") - } - - g.P("func (x *", messagerName, ") Check(hub *", hubTypeIdent, ") error {") +func generateCheck(g *protogen.GeneratedFile, messagerName string) { + g.P("func (x *", messagerName, ") Check(hub *tableau.Hub) error {") g.P("// TODO: implement here.") g.P("return nil") g.P("}") g.P() +} - g.P("func (x *", messagerName, ") CheckCompatibility(hub, newHub *", hubTypeIdent, ") error {") +func generateCheckCompatibility(g *protogen.GeneratedFile, messagerName string) { + g.P("func (x *", messagerName, ") CheckCompatibility(hub, newHub *tableau.Hub) error {") g.P("// TODO: implement here.") g.P("return nil") g.P("}") diff --git a/cmd/protoc-gen-go-tableau-checker/helper.go b/cmd/protoc-gen-go-tableau-checker/helper.go index b3eeffe..352bfd7 100644 --- a/cmd/protoc-gen-go-tableau-checker/helper.go +++ b/cmd/protoc-gen-go-tableau-checker/helper.go @@ -13,8 +13,7 @@ import ( const checkExt = "check" // protoconf file extension const pbExt = "pb" // protobuf file extension -func generateFileHeader(gen *protogen.Plugin, file *protogen.File, g *protogen.GeneratedFile, doNotEdit bool) { - generateCommonHeader(gen, g, doNotEdit) +func generateFileHeader(file *protogen.File, g *protogen.GeneratedFile) { if file.Proto.GetOptions().GetDeprecated() { g.P("// ", file.Desc.Path(), " is a deprecated file.") } else { @@ -22,11 +21,8 @@ func generateFileHeader(gen *protogen.Plugin, file *protogen.File, g *protogen.G } } -func generateCommonHeader(gen *protogen.Plugin, g *protogen.GeneratedFile, doNotEdit bool) { - heading := "// Code generated by protoc-gen-go-tableau-checker." - if doNotEdit { - heading += " DO NOT EDIT." - } +func generateCommonHeader(gen *protogen.Plugin, g *protogen.GeneratedFile) { + heading := "// Code generated by protoc-gen-go-tableau-checker. DO NOT EDIT." g.P(heading) g.P("// versions:") g.P("// - protoc-gen-go-tableau-checker v", version) diff --git a/cmd/protoc-gen-go-tableau-checker/hub.go b/cmd/protoc-gen-go-tableau-checker/hub.go index 473c4b5..e6e0318 100644 --- a/cmd/protoc-gen-go-tableau-checker/hub.go +++ b/cmd/protoc-gen-go-tableau-checker/hub.go @@ -10,7 +10,7 @@ import ( func generateHub(gen *protogen.Plugin) { filename := filepath.Join("hub." + checkExt + ".go") g := gen.NewGeneratedFile(filename, "") - generateCommonHeader(gen, g, true) + generateCommonHeader(gen, g) g.P() g.P("package ", params.pkg) g.P("import (") diff --git a/cmd/protoc-gen-go-tableau-checker/main.go b/cmd/protoc-gen-go-tableau-checker/main.go index af4c549..9b8ce60 100644 --- a/cmd/protoc-gen-go-tableau-checker/main.go +++ b/cmd/protoc-gen-go-tableau-checker/main.go @@ -17,6 +17,8 @@ type Params struct { var params = Params{} +var loaderImportPath protogen.GoImportPath + func main() { var flags flag.FlagSet flags.StringVar(¶ms.pkg, "pkg", "check", "tableau checker package name") @@ -32,8 +34,11 @@ func main() { if !NeedGenFile(f) { continue } - generateMessager(gen, f) + loaderImportPath = protogen.GoImportPath(string(f.GoImportPath) + "/" + params.loaderPkg) + break } + generateTypes(gen) + generateMessager(gen) generateHub(gen) return nil }) diff --git a/cmd/protoc-gen-go-tableau-checker/types.go b/cmd/protoc-gen-go-tableau-checker/types.go new file mode 100644 index 0000000..33f73c4 --- /dev/null +++ b/cmd/protoc-gen-go-tableau-checker/types.go @@ -0,0 +1,50 @@ +package main + +import ( + "path/filepath" + + "github.com/tableauio/tableau/proto/tableaupb" + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/descriptorpb" +) + +// generateTypes generates related types files. +func generateTypes(gen *protogen.Plugin) { + filename := filepath.Join("types." + checkExt + ".go") + g := gen.NewGeneratedFile(filename, "") + generateCommonHeader(gen, g) + g.P() + g.P("package ", params.pkg) + var messages []*protogen.Message + for _, f := range gen.Files { + if !NeedGenFile(f) { + continue + } + for _, message := range f.Messages { + opts := message.Desc.Options().(*descriptorpb.MessageOptions) + worksheet := proto.GetExtension(opts, tableaupb.E_Worksheet).(*tableaupb.WorksheetOptions) + if worksheet != nil { + messages = append(messages, message) + } + } + } + for _, message := range messages { + messagerName := string(message.Desc.Name()) + g.P() + // messager definition + g.P("type ", messagerName, " struct {") + g.P(loaderImportPath.Ident(messagerName)) + g.P("}") + } + g.P() + // register messagers + g.P("func init() {") + for _, message := range messages { + messagerName := string(message.Desc.Name()) + g.P("register(func() checker {") + g.P("return new(", messagerName, ")") + g.P("})") + } + g.P("}") +} diff --git a/test/check/test_conf.check.go b/test/check/test_conf.check.go index b621888..ea86240 100644 --- a/test/check/test_conf.check.go +++ b/test/check/test_conf.check.go @@ -1,7 +1,3 @@ -// Code generated by protoc-gen-go-tableau-checker. -// versions: -// - protoc-gen-go-tableau-checker v0.6.2 -// - protoc v3.19.3 // source: test_conf.proto package check @@ -12,10 +8,6 @@ import ( tableau "github.com/tableauio/checker/test/protoconf/tableau" ) -type ActivityConf struct { - tableau.ActivityConf -} - func (x *ActivityConf) Check(hub *tableau.Hub) error { // TODO: implement here. return nil @@ -26,10 +18,6 @@ func (x *ActivityConf) CheckCompatibility(hub, newHub *tableau.Hub) error { hub.GetItemConf().Data(), newHub.GetItemConf().Data()) } -type ChapterConf struct { - tableau.ChapterConf -} - func (x *ChapterConf) Check(hub *tableau.Hub) error { // TODO: implement here. return nil @@ -39,10 +27,6 @@ func (x *ChapterConf) CheckCompatibility(hub, newHub *tableau.Hub) error { return fmt.Errorf("should not reach here since ChapterConf is not successfully loaded") } -type ThemeConf struct { - tableau.ThemeConf -} - func (x *ThemeConf) Check(hub *tableau.Hub) error { // TODO: implement here. return nil @@ -51,16 +35,3 @@ func (x *ThemeConf) Check(hub *tableau.Hub) error { func (x *ThemeConf) CheckCompatibility(hub, newHub *tableau.Hub) error { return fmt.Errorf("should not reach here since ThemeConf is not successfully loaded") } - -func init() { - // NOTE: This func is auto-generated. DO NOT EDIT. - register(func() checker { - return new(ActivityConf) - }) - register(func() checker { - return new(ChapterConf) - }) - register(func() checker { - return new(ThemeConf) - }) -} diff --git a/test/check/types.check.go b/test/check/types.check.go new file mode 100644 index 0000000..93ef7c8 --- /dev/null +++ b/test/check/types.check.go @@ -0,0 +1,35 @@ +// Code generated by protoc-gen-go-tableau-checker. DO NOT EDIT. +// versions: +// - protoc-gen-go-tableau-checker v0.6.2 +// - protoc v3.19.3 + +package check + +import ( + tableau "github.com/tableauio/checker/test/protoconf/tableau" +) + +type ActivityConf struct { + tableau.ActivityConf +} + +type ChapterConf struct { + tableau.ChapterConf +} + +type ThemeConf struct { + tableau.ThemeConf +} + +func init() { + // NOTE: This func is auto-generated. DO NOT EDIT. + register(func() checker { + return new(ActivityConf) + }) + register(func() checker { + return new(ChapterConf) + }) + register(func() checker { + return new(ThemeConf) + }) +}