blob: f499a8d7b5d8b3ca414085c6ee9d91aa1352fbe4 [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"
23 "strings"
24
25 "github.com/golang/protobuf/proto"
26 descpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
27 pluginpb "github.com/golang/protobuf/protoc-gen-go/plugin"
28)
29
30// Run executes a function as a protoc plugin.
31//
32// It reads a CodeGeneratorRequest message from os.Stdin, invokes the plugin
33// function, and writes a CodeGeneratorResponse message to os.Stdout.
34//
35// If a failure occurs while reading or writing, Run prints an error to
36// os.Stderr and calls os.Exit(1).
37func Run(f func(*Plugin) error) {
38 if err := run(f); err != nil {
39 fmt.Fprintf(os.Stderr, "%s: %v\n", filepath.Base(os.Args[0]), err)
40 os.Exit(1)
41 }
42}
43
44func run(f func(*Plugin) error) error {
45 in, err := ioutil.ReadAll(os.Stdin)
46 if err != nil {
47 return err
48 }
49 req := &pluginpb.CodeGeneratorRequest{}
50 if err := proto.Unmarshal(in, req); err != nil {
51 return err
52 }
53 gen, err := New(req)
54 if err != nil {
55 return err
56 }
57 if err := f(gen); err != nil {
58 // Errors from the plugin function are reported by setting the
59 // error field in the CodeGeneratorResponse.
60 //
61 // In contrast, errors that indicate a problem in protoc
62 // itself (unparsable input, I/O errors, etc.) are reported
63 // to stderr.
64 gen.Error(err)
65 }
66 resp := gen.Response()
67 out, err := proto.Marshal(resp)
68 if err != nil {
69 return err
70 }
71 if _, err := os.Stdout.Write(out); err != nil {
72 return err
73 }
74 return nil
75}
76
77// A Plugin is a protoc plugin invocation.
78type Plugin struct {
79 // Request is the CodeGeneratorRequest provided by protoc.
80 Request *pluginpb.CodeGeneratorRequest
81
82 // Files is the set of files to generate and everything they import.
83 // Files appear in topological order, so each file appears before any
84 // file that imports it.
85 Files []*File
86 filesByName map[string]*File
87
88 packageImportPath string // Go import path of the package we're generating code for.
89
90 genFiles []*GeneratedFile
91 err error
92}
93
94// New returns a new Plugin.
95func New(req *pluginpb.CodeGeneratorRequest) (*Plugin, error) {
96 gen := &Plugin{
97 Request: req,
98 filesByName: make(map[string]*File),
99 }
100
101 // TODO: Figure out how to pass parameters to the generator.
102 for _, param := range strings.Split(req.GetParameter(), ",") {
103 var value string
104 if i := strings.Index(param, "="); i >= 0 {
105 value = param[i+1:]
106 param = param[0:i]
107 }
108 switch param {
109 case "":
110 // Ignore.
111 case "import_prefix":
112 // TODO
113 case "import_path":
114 gen.packageImportPath = value
115 case "paths":
116 // TODO
117 case "plugins":
118 // TODO
119 case "annotate_code":
120 // TODO
121 default:
122 if param[0] != 'M' {
123 return nil, fmt.Errorf("unknown parameter %q", param)
124 }
125 // TODO
126 }
127 }
128
129 for _, fdesc := range gen.Request.ProtoFile {
130 f := newFile(gen, fdesc)
131 name := f.Desc.GetName()
132 if gen.filesByName[name] != nil {
133 return nil, fmt.Errorf("duplicate file name: %q", name)
134 }
135 gen.Files = append(gen.Files, f)
136 gen.filesByName[name] = f
137 }
138 for _, name := range gen.Request.FileToGenerate {
139 f, ok := gen.FileByName(name)
140 if !ok {
141 return nil, fmt.Errorf("no descriptor for generated file: %v", name)
142 }
143 f.Generate = true
144 }
145 return gen, nil
146}
147
148// Error records an error in code generation. The generator will report the
149// error back to protoc and will not produce output.
150func (gen *Plugin) Error(err error) {
151 if gen.err == nil {
152 gen.err = err
153 }
154}
155
156// Response returns the generator output.
157func (gen *Plugin) Response() *pluginpb.CodeGeneratorResponse {
158 resp := &pluginpb.CodeGeneratorResponse{}
159 if gen.err != nil {
160 resp.Error = proto.String(gen.err.Error())
161 return resp
162 }
163 for _, gf := range gen.genFiles {
Damien Neilc7d07d92018-08-22 13:46:02 -0700164 content, err := gf.Content()
165 if err != nil {
166 return &pluginpb.CodeGeneratorResponse{
167 Error: proto.String(err.Error()),
168 }
169 }
Damien Neil220c2022018-08-15 11:24:18 -0700170 resp.File = append(resp.File, &pluginpb.CodeGeneratorResponse_File{
171 Name: proto.String(gf.path),
Damien Neilc7d07d92018-08-22 13:46:02 -0700172 Content: proto.String(string(content)),
Damien Neil220c2022018-08-15 11:24:18 -0700173 })
174 }
175 return resp
176}
177
178// FileByName returns the file with the given name.
179func (gen *Plugin) FileByName(name string) (f *File, ok bool) {
180 f, ok = gen.filesByName[name]
181 return f, ok
182}
183
Damien Neilc7d07d92018-08-22 13:46:02 -0700184// A File describes a .proto source file.
Damien Neil220c2022018-08-15 11:24:18 -0700185type File struct {
Damien Neilc7d07d92018-08-22 13:46:02 -0700186 Desc *descpb.FileDescriptorProto // TODO: protoreflect.FileDescriptor
Damien Neil220c2022018-08-15 11:24:18 -0700187
Damien Neilc7d07d92018-08-22 13:46:02 -0700188 Messages []*Message // top-level message declartions
189 Generate bool // true if we should generate code for this file
Damien Neil220c2022018-08-15 11:24:18 -0700190}
191
192func newFile(gen *Plugin, p *descpb.FileDescriptorProto) *File {
Damien Neilc7d07d92018-08-22 13:46:02 -0700193 f := &File{
Damien Neil220c2022018-08-15 11:24:18 -0700194 Desc: p,
195 }
Damien Neilc7d07d92018-08-22 13:46:02 -0700196 for _, d := range p.MessageType {
197 f.Messages = append(f.Messages, newMessage(gen, nil, d))
198 }
199 return f
200}
201
202// A Message describes a message.
203type Message struct {
204 Desc *descpb.DescriptorProto // TODO: protoreflect.MessageDescriptor
205
206 GoIdent GoIdent // name of the generated Go type
207 Messages []*Message // nested message declarations
208}
209
210func newMessage(gen *Plugin, parent *Message, p *descpb.DescriptorProto) *Message {
211 m := &Message{
212 Desc: p,
213 GoIdent: camelCase(p.GetName()),
214 }
215 if parent != nil {
216 m.GoIdent = parent.GoIdent + "_" + m.GoIdent
217 }
218 for _, nested := range p.GetNestedType() {
219 m.Messages = append(m.Messages, newMessage(gen, m, nested))
220 }
221 return m
Damien Neil220c2022018-08-15 11:24:18 -0700222}
223
224// A GeneratedFile is a generated file.
225type GeneratedFile struct {
226 path string
227 buf bytes.Buffer
228}
229
230// NewGeneratedFile creates a new generated file with the given path.
231func (gen *Plugin) NewGeneratedFile(path string) *GeneratedFile {
232 g := &GeneratedFile{
233 path: path,
234 }
235 gen.genFiles = append(gen.genFiles, g)
236 return g
237}
238
239// P prints a line to the generated output. It converts each parameter to a
240// string following the same rules as fmt.Print. It never inserts spaces
241// between parameters.
242//
243// TODO: .meta file annotations.
244func (g *GeneratedFile) P(v ...interface{}) {
245 for _, x := range v {
246 fmt.Fprint(&g.buf, x)
247 }
248 fmt.Fprintln(&g.buf)
249}
250
251// Write implements io.Writer.
252func (g *GeneratedFile) Write(p []byte) (n int, err error) {
253 return g.buf.Write(p)
254}
255
256// Content returns the contents of the generated file.
Damien Neilc7d07d92018-08-22 13:46:02 -0700257func (g *GeneratedFile) Content() ([]byte, error) {
258 if !strings.HasSuffix(g.path, ".go") {
259 return g.buf.Bytes(), nil
260 }
261
262 // Reformat generated code.
263 original := g.buf.Bytes()
264 fset := token.NewFileSet()
265 ast, err := parser.ParseFile(fset, "", original, parser.ParseComments)
266 if err != nil {
267 // Print out the bad code with line numbers.
268 // This should never happen in practice, but it can while changing generated code
269 // so consider this a debugging aid.
270 var src bytes.Buffer
271 s := bufio.NewScanner(bytes.NewReader(original))
272 for line := 1; s.Scan(); line++ {
273 fmt.Fprintf(&src, "%5d\t%s\n", line, s.Bytes())
274 }
275 return nil, fmt.Errorf("%v: unparsable Go source: %v\n%v", g.path, err, src.String())
276 }
277 var out bytes.Buffer
278 if err = (&printer.Config{Mode: printer.TabIndent | printer.UseSpaces, Tabwidth: 8}).Fprint(&out, fset, ast); err != nil {
279 return nil, fmt.Errorf("%v: can not reformat Go source: %v", g.path, err)
280 }
281 // TODO: Patch annotation locations.
282 return out.Bytes(), nil
283
Damien Neil220c2022018-08-15 11:24:18 -0700284}