blob: 2d65ee03807de5f36313accad74e1d7a201e222f [file] [log] [blame]
Damien Neil220c2022018-08-15 11:24:18 -07001// Copyright 2018 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// Package protogen provides support for writing protoc plugins.
6//
7// Plugins for protoc, the Protocol Buffers Compiler, are programs which read
8// a CodeGeneratorRequest protocol buffer from standard input and write a
9// CodeGeneratorResponse protocol buffer to standard output. This package
10// provides support for writing plugins which generate Go code.
11package protogen
12
13import (
Damien Neilc7d07d92018-08-22 13:46:02 -070014 "bufio"
Damien Neil220c2022018-08-15 11:24:18 -070015 "bytes"
16 "fmt"
Damien Neilc7d07d92018-08-22 13:46:02 -070017 "go/parser"
18 "go/printer"
19 "go/token"
Damien Neil220c2022018-08-15 11:24:18 -070020 "io/ioutil"
21 "os"
22 "path/filepath"
Damien Neild9016772018-08-23 14:39:30 -070023 "sort"
24 "strconv"
Damien Neil220c2022018-08-15 11:24:18 -070025 "strings"
26
27 "github.com/golang/protobuf/proto"
28 descpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
29 pluginpb "github.com/golang/protobuf/protoc-gen-go/plugin"
Damien Neild9016772018-08-23 14:39:30 -070030 "golang.org/x/tools/go/ast/astutil"
Damien Neil220c2022018-08-15 11:24:18 -070031)
32
33// Run executes a function as a protoc plugin.
34//
35// It reads a CodeGeneratorRequest message from os.Stdin, invokes the plugin
36// function, and writes a CodeGeneratorResponse message to os.Stdout.
37//
38// If a failure occurs while reading or writing, Run prints an error to
39// os.Stderr and calls os.Exit(1).
40func Run(f func(*Plugin) error) {
41 if err := run(f); err != nil {
42 fmt.Fprintf(os.Stderr, "%s: %v\n", filepath.Base(os.Args[0]), err)
43 os.Exit(1)
44 }
45}
46
47func run(f func(*Plugin) error) error {
48 in, err := ioutil.ReadAll(os.Stdin)
49 if err != nil {
50 return err
51 }
52 req := &pluginpb.CodeGeneratorRequest{}
53 if err := proto.Unmarshal(in, req); err != nil {
54 return err
55 }
56 gen, err := New(req)
57 if err != nil {
58 return err
59 }
60 if err := f(gen); err != nil {
61 // Errors from the plugin function are reported by setting the
62 // error field in the CodeGeneratorResponse.
63 //
64 // In contrast, errors that indicate a problem in protoc
65 // itself (unparsable input, I/O errors, etc.) are reported
66 // to stderr.
67 gen.Error(err)
68 }
69 resp := gen.Response()
70 out, err := proto.Marshal(resp)
71 if err != nil {
72 return err
73 }
74 if _, err := os.Stdout.Write(out); err != nil {
75 return err
76 }
77 return nil
78}
79
80// A Plugin is a protoc plugin invocation.
81type Plugin struct {
82 // Request is the CodeGeneratorRequest provided by protoc.
83 Request *pluginpb.CodeGeneratorRequest
84
85 // Files is the set of files to generate and everything they import.
86 // Files appear in topological order, so each file appears before any
87 // file that imports it.
88 Files []*File
89 filesByName map[string]*File
90
91 packageImportPath string // Go import path of the package we're generating code for.
92
93 genFiles []*GeneratedFile
94 err error
95}
96
97// New returns a new Plugin.
98func New(req *pluginpb.CodeGeneratorRequest) (*Plugin, error) {
99 gen := &Plugin{
100 Request: req,
101 filesByName: make(map[string]*File),
102 }
103
104 // TODO: Figure out how to pass parameters to the generator.
105 for _, param := range strings.Split(req.GetParameter(), ",") {
106 var value string
107 if i := strings.Index(param, "="); i >= 0 {
108 value = param[i+1:]
109 param = param[0:i]
110 }
111 switch param {
112 case "":
113 // Ignore.
114 case "import_prefix":
115 // TODO
116 case "import_path":
117 gen.packageImportPath = value
118 case "paths":
119 // TODO
120 case "plugins":
121 // TODO
122 case "annotate_code":
123 // TODO
124 default:
125 if param[0] != 'M' {
126 return nil, fmt.Errorf("unknown parameter %q", param)
127 }
128 // TODO
129 }
130 }
131
132 for _, fdesc := range gen.Request.ProtoFile {
133 f := newFile(gen, fdesc)
134 name := f.Desc.GetName()
135 if gen.filesByName[name] != nil {
136 return nil, fmt.Errorf("duplicate file name: %q", name)
137 }
138 gen.Files = append(gen.Files, f)
139 gen.filesByName[name] = f
140 }
141 for _, name := range gen.Request.FileToGenerate {
142 f, ok := gen.FileByName(name)
143 if !ok {
144 return nil, fmt.Errorf("no descriptor for generated file: %v", name)
145 }
146 f.Generate = true
147 }
148 return gen, nil
149}
150
151// Error records an error in code generation. The generator will report the
152// error back to protoc and will not produce output.
153func (gen *Plugin) Error(err error) {
154 if gen.err == nil {
155 gen.err = err
156 }
157}
158
159// Response returns the generator output.
160func (gen *Plugin) Response() *pluginpb.CodeGeneratorResponse {
161 resp := &pluginpb.CodeGeneratorResponse{}
162 if gen.err != nil {
163 resp.Error = proto.String(gen.err.Error())
164 return resp
165 }
166 for _, gf := range gen.genFiles {
Damien Neilc7d07d92018-08-22 13:46:02 -0700167 content, err := gf.Content()
168 if err != nil {
169 return &pluginpb.CodeGeneratorResponse{
170 Error: proto.String(err.Error()),
171 }
172 }
Damien Neil220c2022018-08-15 11:24:18 -0700173 resp.File = append(resp.File, &pluginpb.CodeGeneratorResponse_File{
Damien Neild9016772018-08-23 14:39:30 -0700174 Name: proto.String(gf.filename),
Damien Neilc7d07d92018-08-22 13:46:02 -0700175 Content: proto.String(string(content)),
Damien Neil220c2022018-08-15 11:24:18 -0700176 })
177 }
178 return resp
179}
180
181// FileByName returns the file with the given name.
182func (gen *Plugin) FileByName(name string) (f *File, ok bool) {
183 f, ok = gen.filesByName[name]
184 return f, ok
185}
186
Damien Neilc7d07d92018-08-22 13:46:02 -0700187// A File describes a .proto source file.
Damien Neil220c2022018-08-15 11:24:18 -0700188type File struct {
Damien Neilc7d07d92018-08-22 13:46:02 -0700189 Desc *descpb.FileDescriptorProto // TODO: protoreflect.FileDescriptor
Damien Neil220c2022018-08-15 11:24:18 -0700190
Damien Neild9016772018-08-23 14:39:30 -0700191 GoImportPath GoImportPath // import path of this file's Go package
192 Messages []*Message // top-level message declarations
193 Generate bool // true if we should generate code for this file
Damien Neil220c2022018-08-15 11:24:18 -0700194}
195
196func newFile(gen *Plugin, p *descpb.FileDescriptorProto) *File {
Damien Neilc7d07d92018-08-22 13:46:02 -0700197 f := &File{
Damien Neil220c2022018-08-15 11:24:18 -0700198 Desc: p,
199 }
Damien Neild9016772018-08-23 14:39:30 -0700200 for i, mdesc := range p.MessageType {
201 f.Messages = append(f.Messages, newMessage(gen, f, nil, mdesc, i))
Damien Neilc7d07d92018-08-22 13:46:02 -0700202 }
203 return f
204}
205
206// A Message describes a message.
207type Message struct {
208 Desc *descpb.DescriptorProto // TODO: protoreflect.MessageDescriptor
209
210 GoIdent GoIdent // name of the generated Go type
211 Messages []*Message // nested message declarations
212}
213
Damien Neild9016772018-08-23 14:39:30 -0700214func newMessage(gen *Plugin, f *File, parent *Message, p *descpb.DescriptorProto, index int) *Message {
Damien Neilc7d07d92018-08-22 13:46:02 -0700215 m := &Message{
Damien Neild9016772018-08-23 14:39:30 -0700216 Desc: p,
217 GoIdent: GoIdent{
218 GoName: camelCase(p.GetName()),
219 GoImportPath: f.GoImportPath,
220 },
Damien Neilc7d07d92018-08-22 13:46:02 -0700221 }
222 if parent != nil {
Damien Neild9016772018-08-23 14:39:30 -0700223 m.GoIdent.GoName = parent.GoIdent.GoName + "_" + m.GoIdent.GoName
Damien Neilc7d07d92018-08-22 13:46:02 -0700224 }
Damien Neild9016772018-08-23 14:39:30 -0700225 for i, nested := range p.GetNestedType() {
226 m.Messages = append(m.Messages, newMessage(gen, f, m, nested, i))
Damien Neilc7d07d92018-08-22 13:46:02 -0700227 }
228 return m
Damien Neil220c2022018-08-15 11:24:18 -0700229}
230
231// A GeneratedFile is a generated file.
232type GeneratedFile struct {
Damien Neild9016772018-08-23 14:39:30 -0700233 filename string
234 goImportPath GoImportPath
235 buf bytes.Buffer
236 packageNames map[GoImportPath]GoPackageName
237 usedPackageNames map[GoPackageName]bool
Damien Neil220c2022018-08-15 11:24:18 -0700238}
239
Damien Neild9016772018-08-23 14:39:30 -0700240// NewGeneratedFile creates a new generated file with the given filename
241// and import path.
242func (gen *Plugin) NewGeneratedFile(filename string, goImportPath GoImportPath) *GeneratedFile {
Damien Neil220c2022018-08-15 11:24:18 -0700243 g := &GeneratedFile{
Damien Neild9016772018-08-23 14:39:30 -0700244 filename: filename,
245 goImportPath: goImportPath,
246 packageNames: make(map[GoImportPath]GoPackageName),
247 usedPackageNames: make(map[GoPackageName]bool),
Damien Neil220c2022018-08-15 11:24:18 -0700248 }
249 gen.genFiles = append(gen.genFiles, g)
250 return g
251}
252
253// P prints a line to the generated output. It converts each parameter to a
254// string following the same rules as fmt.Print. It never inserts spaces
255// between parameters.
256//
257// TODO: .meta file annotations.
258func (g *GeneratedFile) P(v ...interface{}) {
259 for _, x := range v {
Damien Neild9016772018-08-23 14:39:30 -0700260 switch x := x.(type) {
261 case GoIdent:
262 if x.GoImportPath != g.goImportPath {
263 fmt.Fprint(&g.buf, g.goPackageName(x.GoImportPath))
264 fmt.Fprint(&g.buf, ".")
265 }
266 fmt.Fprint(&g.buf, x.GoName)
267 default:
268 fmt.Fprint(&g.buf, x)
269 }
Damien Neil220c2022018-08-15 11:24:18 -0700270 }
271 fmt.Fprintln(&g.buf)
272}
273
Damien Neild9016772018-08-23 14:39:30 -0700274func (g *GeneratedFile) goPackageName(importPath GoImportPath) GoPackageName {
275 if name, ok := g.packageNames[importPath]; ok {
276 return name
277 }
278 name := cleanPackageName(baseName(string(importPath)))
279 for i, orig := 1, name; g.usedPackageNames[name]; i++ {
280 name = orig + GoPackageName(strconv.Itoa(i))
281 }
282 g.packageNames[importPath] = name
283 g.usedPackageNames[name] = true
284 return name
285}
286
Damien Neil220c2022018-08-15 11:24:18 -0700287// Write implements io.Writer.
288func (g *GeneratedFile) Write(p []byte) (n int, err error) {
289 return g.buf.Write(p)
290}
291
292// Content returns the contents of the generated file.
Damien Neilc7d07d92018-08-22 13:46:02 -0700293func (g *GeneratedFile) Content() ([]byte, error) {
Damien Neild9016772018-08-23 14:39:30 -0700294 if !strings.HasSuffix(g.filename, ".go") {
Damien Neilc7d07d92018-08-22 13:46:02 -0700295 return g.buf.Bytes(), nil
296 }
297
298 // Reformat generated code.
299 original := g.buf.Bytes()
300 fset := token.NewFileSet()
301 ast, err := parser.ParseFile(fset, "", original, parser.ParseComments)
302 if err != nil {
303 // Print out the bad code with line numbers.
304 // This should never happen in practice, but it can while changing generated code
305 // so consider this a debugging aid.
306 var src bytes.Buffer
307 s := bufio.NewScanner(bytes.NewReader(original))
308 for line := 1; s.Scan(); line++ {
309 fmt.Fprintf(&src, "%5d\t%s\n", line, s.Bytes())
310 }
Damien Neild9016772018-08-23 14:39:30 -0700311 return nil, fmt.Errorf("%v: unparsable Go source: %v\n%v", g.filename, err, src.String())
Damien Neilc7d07d92018-08-22 13:46:02 -0700312 }
Damien Neild9016772018-08-23 14:39:30 -0700313
314 // Add imports.
315 var importPaths []string
316 for importPath := range g.packageNames {
317 importPaths = append(importPaths, string(importPath))
318 }
319 sort.Strings(importPaths)
320 for _, importPath := range importPaths {
321 astutil.AddNamedImport(fset, ast, string(g.packageNames[GoImportPath(importPath)]), importPath)
322 }
323
Damien Neilc7d07d92018-08-22 13:46:02 -0700324 var out bytes.Buffer
325 if err = (&printer.Config{Mode: printer.TabIndent | printer.UseSpaces, Tabwidth: 8}).Fprint(&out, fset, ast); err != nil {
Damien Neild9016772018-08-23 14:39:30 -0700326 return nil, fmt.Errorf("%v: can not reformat Go source: %v", g.filename, err)
Damien Neilc7d07d92018-08-22 13:46:02 -0700327 }
Damien Neild9016772018-08-23 14:39:30 -0700328 // TODO: Annotations.
Damien Neilc7d07d92018-08-22 13:46:02 -0700329 return out.Bytes(), nil
330
Damien Neil220c2022018-08-15 11:24:18 -0700331}