go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/starlark/starlarkproto/descriptor_set.go (about)

     1  // Copyright 2019 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package starlarkproto
    16  
    17  import (
    18  	"fmt"
    19  	"sync/atomic"
    20  
    21  	"go.starlark.net/starlark"
    22  
    23  	"google.golang.org/protobuf/types/descriptorpb"
    24  )
    25  
    26  // DescriptorSet contains FileDescriptorProto of zero or more *.proto files,
    27  // along with pointers to DescriptorSets with their imports.
    28  //
    29  // A descriptor set can be registered in a Loader. Doing so transitively
    30  // registers all its dependencies. See Loader.AddDescriptorSet for more details.
    31  //
    32  // Implements starlark.Value and starlark.HasAttrs interfaces. Usage from
    33  // Starlark side may look like this:
    34  //
    35  //	load(".../wellknown_descpb.star", "wellknown_descpb")
    36  //	myprotos_descpb = proto.new_descriptor_set(
    37  //	    name = "myprotos",
    38  //	    deps = [wellknown_descpb],
    39  //	    blob = io.read_file("myprotos.descpb"),
    40  //	)
    41  //	myprotos_descpb.register()
    42  //
    43  // By default register() registers the descriptor set in the default loader,
    44  // i.e. ds.register() is same as ds.register(loader=proto.default_loader()).
    45  // Also note that ds.register(loader=l) is a sugar for l.add_descriptor_set(ds),
    46  // so ds.register() is same as proto.default_loader().add_descriptor_set(ds),
    47  // just much shorter.
    48  type DescriptorSet struct {
    49  	name string // for debug messages and errors, can be anything
    50  	hash uint32 // unique (within the process) value, used by Hash()
    51  
    52  	fdps []*descriptorpb.FileDescriptorProto // files in this set
    53  	deps []*DescriptorSet                    // direct dependencies of this set
    54  }
    55  
    56  // descSetHash is used to give each instance of *DescriptorSet its own unique
    57  // non-reused hash value for Hash() method.
    58  var descSetHash uint32 = 10000
    59  
    60  // NewDescriptorSet evaluates given file descriptors and their dependencies and
    61  // produces new DescriptorSet if there are no unresolved imports and no
    62  // duplicated files.
    63  //
    64  // 'fdps' should be ordered topologically (i.e. if file A imports file B, then
    65  // B should precede A in 'fdps' or be somewhere among 'deps'). This is always
    66  // the case when generating sets via 'protoc --descriptor_set_out=...'.
    67  //
    68  // Note that dependencies can only be specified when creating the descriptor
    69  // set and can't be changed later. Cycles thus are impossible.
    70  func NewDescriptorSet(name string, fdps []*descriptorpb.FileDescriptorProto, deps []*DescriptorSet) (*DescriptorSet, error) {
    71  	ds := &DescriptorSet{
    72  		name: name,
    73  		hash: atomic.AddUint32(&descSetHash, 1),
    74  		fdps: append([]*descriptorpb.FileDescriptorProto(nil), fdps...),
    75  		deps: append([]*DescriptorSet(nil), deps...),
    76  	}
    77  
    78  	// Collect ALL dependencies (most nested first). Note that diamond dependency
    79  	// diagram is fine, but the same *.proto file registered in different sets is
    80  	// not fine: this will be checked below.
    81  
    82  	topo := make([]*DescriptorSet, 0, len(deps))
    83  	seen := make(map[*DescriptorSet]struct{}, len(deps))
    84  
    85  	var visitDep func(d *DescriptorSet)
    86  	visitDep = func(d *DescriptorSet) {
    87  		if _, ok := seen[d]; !ok {
    88  			seen[d] = struct{}{}
    89  			for _, dep := range d.deps {
    90  				visitDep(dep)
    91  			}
    92  			topo = append(topo, d)
    93  		}
    94  	}
    95  	visitDep(ds)
    96  
    97  	// Verify each *.proto file is described by exactly one set, and at the moment
    98  	// it is added (based on topological order), all its dependencies are already
    99  	// present.
   100  
   101  	files := map[string]*DescriptorSet{} // *.proto path => DescriptoSet it is defined in
   102  	for _, dep := range topo {
   103  		for _, fd := range dep.fdps {
   104  			pname := fd.GetName()
   105  			if ds := files[pname]; ds != nil {
   106  				return nil, fmt.Errorf("conflict between descriptor sets %q and %q: both define %q", dep.name, ds.name, pname)
   107  			}
   108  			for _, imp := range fd.GetDependency() {
   109  				if files[imp] == nil {
   110  					return nil, fmt.Errorf("in descriptor set %q: %q imports unknown %q", dep.name, pname, imp)
   111  				}
   112  			}
   113  			files[pname] = dep
   114  		}
   115  	}
   116  
   117  	return ds, nil
   118  }
   119  
   120  // Implementation of starlark.Value and starlark.HasAttrs.
   121  
   122  // String returns str(...) representation of the set, for debug messages.
   123  func (ds *DescriptorSet) String() string { return fmt.Sprintf("proto.DescriptorSet(%q)", ds.name) }
   124  
   125  // Type returns a short string describing the value's type.
   126  func (ds *DescriptorSet) Type() string { return "proto.DescriptorSet" }
   127  
   128  // Freeze does nothing since DescriptorSet is already immutable.
   129  func (ds *DescriptorSet) Freeze() {}
   130  
   131  // Truth returns the truth value of an object.
   132  func (ds *DescriptorSet) Truth() starlark.Bool { return starlark.True }
   133  
   134  // Hash returns unique value associated with this set.
   135  func (ds *DescriptorSet) Hash() (uint32, error) { return ds.hash, nil }
   136  
   137  // AtrrNames lists available attributes.
   138  func (ds *DescriptorSet) AttrNames() []string {
   139  	return []string{
   140  		"register",
   141  	}
   142  }
   143  
   144  // Attr returns an attribute given its name (or nil if not present).
   145  func (ds *DescriptorSet) Attr(name string) (starlark.Value, error) {
   146  	switch name {
   147  	case "register":
   148  		return dsRegisterBuiltin.BindReceiver(ds), nil
   149  	default:
   150  		return nil, nil
   151  	}
   152  }
   153  
   154  // Implementation of DescriptorSet Starlark methods.
   155  
   156  var dsRegisterBuiltin = starlark.NewBuiltin("register", func(th *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   157  	var loader starlark.Value
   158  	if err := starlark.UnpackArgs("register", args, kwargs, "loader?", &loader); err != nil {
   159  		return nil, err
   160  	}
   161  	if loader == nil || loader == starlark.None {
   162  		loader = DefaultLoader(th)
   163  		if loader == nil {
   164  			return nil, fmt.Errorf("register: loader is None and there's no default loader")
   165  		}
   166  	}
   167  	l, ok := loader.(*Loader)
   168  	if !ok {
   169  		return nil, fmt.Errorf("register: for parameter \"loader\": got %s, want proto.Loader", l.Type())
   170  	}
   171  	return starlark.None, l.AddDescriptorSet(b.Receiver().(*DescriptorSet))
   172  })