Some checks are pending
Docs Deploy / build_and_deploy (push) Waiting to run
Generate Docs / cli (push) Waiting to run
Generate Config Doc / cli (push) Waiting to run
Go formatting / go-formatting (push) Waiting to run
Check links / markdown-link-check (push) Waiting to run
Integration / pre-test (push) Waiting to run
Integration / test on (push) Blocked by required conditions
Integration / status (push) Blocked by required conditions
Lint / Lint Go code (push) Waiting to run
Test / test (ubuntu-latest) (push) Waiting to run
317 lines
9.9 KiB
Go
317 lines
9.9 KiB
Go
package protoutil
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
|
|
"github.com/emicklei/proto"
|
|
)
|
|
|
|
// Note: The traversing can also be done with proto.Walk but there's some reasons
|
|
// why I chose the cursor instead:
|
|
//
|
|
// 1. We can abort traversing deeper in the tree at any point. (a post
|
|
// ApplyFunc returning false)
|
|
// 2. We keep track of the parent to have finer grained control over where we
|
|
// are in the tree.
|
|
// 3. We can use pre/post handling.
|
|
|
|
// Modeled heavily after the Apply/Cursor logic in astutil, using proto.Visitee as
|
|
// the common interface, abilities for reflection aren't as rich but can still
|
|
// manage in order to get the job done.
|
|
// Cursor has been augmented to add a couple more methods that can make
|
|
// our life easier.
|
|
|
|
// An ApplyFunc is invoked by Apply for each Visitee n, even if n is nil,
|
|
// before and/or after the node's children, using a Cursor describing
|
|
// the current node and providing operations on it.
|
|
//
|
|
// The return value of ApplyFunc controls the syntax tree traversal.
|
|
// See Apply for details.
|
|
type ApplyFunc func(*Cursor) bool
|
|
|
|
// Apply traverses a syntax tree recursively, starting with root,
|
|
// and calling pre and post for each node as described below.
|
|
// Apply returns the syntax tree, possibly modified.
|
|
//
|
|
// If pre is not nil, it is called for each node before the node's
|
|
// children are traversed (pre-order). If pre returns false, no
|
|
// children are traversed, and post is not called for that node.
|
|
//
|
|
// If post is not nil, and a prior call of pre didn't return false,
|
|
// post is called for each node after its children are traversed
|
|
// (post-order). If post returns false, traversal is terminated and
|
|
// Apply returns immediately.
|
|
func Apply(root proto.Visitee, pre, post ApplyFunc) (result proto.Visitee) {
|
|
parent := &struct{ proto.Visitee }{root}
|
|
defer func() {
|
|
if r := recover(); r != nil && r != abort {
|
|
panic(r)
|
|
}
|
|
result = parent.Visitee
|
|
}()
|
|
a := &application{pre: pre, post: post}
|
|
a.apply(parent, "Visitee", nil, root)
|
|
return
|
|
}
|
|
|
|
var abort = new(int) // singleton, to signal termination of Apply
|
|
|
|
// A Cursor describes a node encountered during Apply.
|
|
// Information about the node and its parent is available
|
|
// from the Node, Parent, Name, and Index methods.
|
|
type Cursor struct {
|
|
parent proto.Visitee // parent (containing a []proto.Visitee slice)
|
|
name string
|
|
iter *iterator
|
|
node proto.Visitee // current node we're applying over
|
|
}
|
|
|
|
type iterator struct {
|
|
index, step int
|
|
}
|
|
|
|
// Index reports the index >= 0 of the current Visitee in the slice of Visitees that
|
|
// contains it, or a value < 0 if the current Visitee is not part of a slice.
|
|
// The index of the current node changes if InsertBefore is called while
|
|
// processing the current node.
|
|
func (c *Cursor) Index() int {
|
|
if c.iter != nil {
|
|
return c.iter.index
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// field returns the current node's parent field value.
|
|
func (c *Cursor) field() reflect.Value {
|
|
return reflect.Indirect(reflect.ValueOf(c.parent)).FieldByName(c.name)
|
|
}
|
|
|
|
// Node returns the current Node.
|
|
func (c *Cursor) Node() proto.Visitee { return c.node }
|
|
|
|
// Parent returns the parent of the current Node.
|
|
func (c *Cursor) Parent() proto.Visitee { return c.parent }
|
|
|
|
// Name returns the name of the parent Node field that contains the current Node.
|
|
// If the parent is a *ast.Package and the current Node is a *ast.File, Name returns
|
|
// the filename for the current Node.
|
|
func (c *Cursor) Name() string { return c.name }
|
|
|
|
// IsLast returns if the current node being traversed is the final node in the
|
|
// slice of nodes. Can be used to determine if a node is the last one.
|
|
func (c *Cursor) IsLast() bool {
|
|
i := c.Index()
|
|
if i < 0 {
|
|
panic("IsLast node not contained in slice")
|
|
}
|
|
v := c.field()
|
|
return i == v.Len()-1
|
|
}
|
|
|
|
// Next returns the next Visitee. Can be used to check the next value
|
|
// before deciding to continue.
|
|
func (c *Cursor) Next() (proto.Visitee, bool) {
|
|
i := c.Index()
|
|
if i < 0 {
|
|
panic("Next node not contained in slice")
|
|
}
|
|
v := c.field()
|
|
if i == v.Len()-1 {
|
|
return nil, false
|
|
}
|
|
var x proto.Visitee
|
|
if e := v.Index(i + 1); e.IsValid() {
|
|
x = e.Interface().(proto.Visitee)
|
|
}
|
|
return x, true
|
|
}
|
|
|
|
// Replace replaces the current Node with n.
|
|
// The replacement node is not walked by Apply.
|
|
func (c *Cursor) Replace(n proto.Visitee) {
|
|
v := c.field()
|
|
if i := c.Index(); i >= 0 {
|
|
v = v.Index(i)
|
|
}
|
|
v.Set(reflect.ValueOf(n))
|
|
}
|
|
|
|
// InsertAfter inserts n after the current Node in its containing slice.
|
|
// If the current Node is not part of a slice, InsertAfter panics.
|
|
// Apply does not walk n.
|
|
func (c *Cursor) InsertAfter(n proto.Visitee) {
|
|
i := c.Index()
|
|
if i < 0 {
|
|
panic("InsertAfter node not contained in slice")
|
|
}
|
|
v := c.field()
|
|
v.Set(reflect.Append(v, reflect.Zero(v.Type().Elem())))
|
|
l := v.Len()
|
|
reflect.Copy(v.Slice(i+2, l), v.Slice(i+1, l))
|
|
v.Index(i + 1).Set(reflect.ValueOf(n))
|
|
c.iter.step++
|
|
}
|
|
|
|
// InsertBefore inserts n before the current Node in its containing slice.
|
|
// If the current Node is not part of a slice, InsertBefore panics.
|
|
// Apply will not walk n.
|
|
func (c *Cursor) InsertBefore(n proto.Visitee) {
|
|
i := c.Index()
|
|
if i < 0 {
|
|
panic("InsertBefore node not contained in slice")
|
|
}
|
|
v := c.field()
|
|
v.Set(reflect.Append(v, reflect.Zero(v.Type().Elem())))
|
|
l := v.Len()
|
|
reflect.Copy(v.Slice(i+1, l), v.Slice(i, l))
|
|
v.Index(i).Set(reflect.ValueOf(n))
|
|
c.iter.index++
|
|
}
|
|
|
|
// application carries all the shared data, so we can pass it around cheaply.
|
|
type application struct {
|
|
pre, post ApplyFunc
|
|
cursor Cursor
|
|
iter iterator
|
|
}
|
|
|
|
func (a *application) apply(parent proto.Visitee, name string, iter *iterator, n proto.Visitee) {
|
|
// don't walk into nil's
|
|
if v := reflect.ValueOf(n); v.Kind() == reflect.Ptr && v.IsNil() {
|
|
return
|
|
}
|
|
|
|
// avoid heap-allocating a new cursor for each apply call; reuse a.cursor instead
|
|
saved := a.cursor
|
|
a.cursor.parent = parent
|
|
a.cursor.name = name
|
|
a.cursor.iter = iter
|
|
a.cursor.node = n
|
|
|
|
if a.pre != nil && !a.pre(&a.cursor) {
|
|
a.cursor = saved
|
|
return
|
|
}
|
|
|
|
// Walk the children.
|
|
// This is the issue with proto. Structure isn't really here in order to be able to
|
|
// visit every component using a distinct interface. They are all Visitee's.
|
|
// Ideally, we could wrap the proto nodes into interfaces that enforce a structure,
|
|
// i.e. Nodes, MessageNodes, ServiceNodes, ProtoNodes, etc.
|
|
// this way, inserting into a slice would be guarded (by error-ing) by the type of the slice.
|
|
//
|
|
// An alternative would be to reflect in the insertion methods for cursor and only allow
|
|
// specific elements per type.
|
|
switch n := n.(type) {
|
|
case *proto.Proto:
|
|
a.applyList(n, "Elements")
|
|
case *proto.Service:
|
|
a.apply(n, "Comment", nil, n.Comment)
|
|
a.applyList(n, "Elements")
|
|
case *proto.RPC:
|
|
a.apply(n, "Comment", nil, n.Comment)
|
|
a.apply(n, "Inline Comment", nil, n.InlineComment)
|
|
a.applyList(n, "Elements")
|
|
case *proto.Message:
|
|
a.apply(n, "Comment", nil, n.Comment)
|
|
a.applyList(n, "Elements")
|
|
case *proto.NormalField:
|
|
a.apply(n, "Comment", nil, n.Comment)
|
|
a.apply(n, "Inline Comment", nil, n.InlineComment)
|
|
a.applyList(n, "Options")
|
|
case *proto.Oneof:
|
|
a.apply(n, "Comment", nil, n.Comment)
|
|
a.applyList(n, "Elements")
|
|
case *proto.OneOfField:
|
|
a.apply(n, "Comment", nil, n.Comment)
|
|
a.apply(n, "Inline Comment", nil, n.InlineComment)
|
|
a.applyList(n, "Options")
|
|
case *proto.Enum:
|
|
a.apply(n, "Comment", nil, n.Comment)
|
|
a.applyList(n, "Elements")
|
|
case *proto.EnumField:
|
|
a.apply(n, "Comment", nil, n.Comment)
|
|
a.apply(n, "Inline Comment", nil, n.InlineComment)
|
|
a.applyList(n, "Elements")
|
|
case *proto.Import:
|
|
a.apply(n, "Comment", nil, n.Comment)
|
|
a.apply(n, "Inline Comment", nil, n.InlineComment)
|
|
case *proto.Option:
|
|
a.apply(n, "Comment", nil, n.Comment)
|
|
a.apply(n, "Inline Comment", nil, n.InlineComment)
|
|
case *proto.Package:
|
|
a.apply(n, "Comment", nil, n.Comment)
|
|
a.apply(n, "Inline Comment", nil, n.InlineComment)
|
|
default:
|
|
// Probably a comment, ignore it.
|
|
}
|
|
|
|
if a.post != nil && !a.post(&a.cursor) {
|
|
panic(abort)
|
|
}
|
|
a.cursor = saved
|
|
}
|
|
|
|
// applyList calls apply on each of the elements of the Visitee.
|
|
func (a *application) applyList(parent proto.Visitee, name string) {
|
|
// avoid heap-allocating a new iterator for each applyList call; reuse a.iter instead
|
|
saved := a.iter
|
|
a.iter.index = 0
|
|
for {
|
|
// must reload parent.name each time, since cursor modifications might change it
|
|
v := reflect.Indirect(reflect.ValueOf(parent)).FieldByName(name)
|
|
if a.iter.index >= v.Len() {
|
|
break
|
|
}
|
|
|
|
// element x may be nil in a bad AST - be cautious
|
|
var x proto.Visitee
|
|
if e := v.Index(a.iter.index); e.IsValid() {
|
|
x = e.Interface().(proto.Visitee)
|
|
}
|
|
|
|
// reset step on each iteration.
|
|
a.iter.step = 1
|
|
a.apply(parent, name, &a.iter, x)
|
|
a.iter.index += a.iter.step
|
|
}
|
|
a.iter = saved
|
|
}
|
|
|
|
// Append appends the elements provided to the node `n`. `n` must be
|
|
// a node that can accept elements, such as a proto.File or a proto.Message.
|
|
//
|
|
// Append panics if `n` is not a node that can accept elements or if the type
|
|
// of the elements provided is not compatible with the type of the elements
|
|
// contained by `n`. (Basically, this applies to NormalFields and OneOfFields,).
|
|
func Append(n proto.Visitee, elems ...proto.Visitee) {
|
|
// return early if the slice is empty.
|
|
if len(elems) == 0 {
|
|
return
|
|
}
|
|
switch n.(type) {
|
|
case *proto.Proto, *proto.Message, *proto.Enum, *proto.Oneof,
|
|
*proto.Service, *proto.EnumField, *proto.RPC:
|
|
// Can just append directly.
|
|
v := reflect.Indirect(reflect.ValueOf(n)).FieldByName("Elements")
|
|
v.Set(reflect.AppendSlice(v, reflect.ValueOf(elems)))
|
|
case *proto.NormalField, *proto.OneOfField:
|
|
// Make into options, panic on failure of one of the objects to do
|
|
// so.
|
|
var elements []*proto.Option
|
|
for _, e := range elems {
|
|
o, ok := e.(*proto.Option)
|
|
if !ok {
|
|
panic(fmt.Sprintf("Tried to append %T to a slice of Options", e))
|
|
}
|
|
elements = append(elements, o)
|
|
}
|
|
// append
|
|
v := reflect.Indirect(reflect.ValueOf(n)).FieldByName("Options")
|
|
v.Set(reflect.AppendSlice(v, reflect.ValueOf(elements)))
|
|
return
|
|
default:
|
|
panic("Append: node not a slice")
|
|
}
|
|
}
|