Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions cmd/protoc-gen-go-tableau-checker/ast.go
Original file line number Diff line number Diff line change
@@ -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
}
180 changes: 54 additions & 126 deletions cmd/protoc-gen-go-tableau-checker/check.go
Original file line number Diff line number Diff line change
@@ -1,176 +1,104 @@
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"
"google.golang.org/protobuf/proto"
"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("}")
Expand Down
10 changes: 3 additions & 7 deletions cmd/protoc-gen-go-tableau-checker/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,16 @@ 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 {
g.P("// source: ", file.Desc.Path())
}
}

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)
Expand Down
2 changes: 1 addition & 1 deletion cmd/protoc-gen-go-tableau-checker/hub.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (")
Expand Down
7 changes: 6 additions & 1 deletion cmd/protoc-gen-go-tableau-checker/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ type Params struct {

var params = Params{}

var loaderImportPath protogen.GoImportPath

func main() {
var flags flag.FlagSet
flags.StringVar(&params.pkg, "pkg", "check", "tableau checker package name")
Expand All @@ -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
})
Expand Down
50 changes: 50 additions & 0 deletions cmd/protoc-gen-go-tableau-checker/types.go
Original file line number Diff line number Diff line change
@@ -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("}")
}
Loading