| package protoparse |
| |
| import ( |
| "bytes" |
| "fmt" |
| "sort" |
| "strings" |
| |
| "github.com/golang/protobuf/proto" |
| dpb "github.com/golang/protobuf/protoc-gen-go/descriptor" |
| |
| "github.com/jhump/protoreflect/desc" |
| "github.com/jhump/protoreflect/desc/internal" |
| ) |
| |
| type linker struct { |
| files map[string]*parseResult |
| descriptorPool map[*dpb.FileDescriptorProto]map[string]proto.Message |
| extensions map[string]map[int32]string |
| } |
| |
| func newLinker(files map[string]*parseResult) *linker { |
| return &linker{files: files} |
| } |
| |
| func (l *linker) linkFiles() (map[string]*desc.FileDescriptor, error) { |
| // First, we put all symbols into a single pool, which lets us ensure there |
| // are no duplicate symbols and will also let us resolve and revise all type |
| // references in next step. |
| if err := l.createDescriptorPool(); err != nil { |
| return nil, err |
| } |
| |
| // After we've populated the pool, we can now try to resolve all type |
| // references. All references must be checked for correct type, any fields |
| // with enum types must be corrected (since we parse them as if they are |
| // message references since we don't actually know message or enum until |
| // link time), and references will be re-written to be fully-qualified |
| // references (e.g. start with a dot "."). |
| if err := l.resolveReferences(); err != nil { |
| return nil, err |
| } |
| |
| // Now we've validated the descriptors, so we can link them into rich |
| // descriptors. This is a little redundant since that step does similar |
| // checking of symbols. But, without breaking encapsulation (e.g. exporting |
| // a lot of fields from desc package that are currently unexported) or |
| // merging this into the same package, we can't really prevent it. |
| linked, err := l.createdLinkedDescriptors() |
| if err != nil { |
| return nil, err |
| } |
| |
| // Now that we have linked descriptors, we can interpret any uninterpreted |
| // options that remain. |
| for _, r := range l.files { |
| fd := linked[r.fd.GetName()] |
| if err := interpretFileOptions(r, richFileDescriptorish{FileDescriptor: fd}); err != nil { |
| return nil, err |
| } |
| } |
| |
| return linked, nil |
| } |
| |
| func (l *linker) createDescriptorPool() error { |
| l.descriptorPool = map[*dpb.FileDescriptorProto]map[string]proto.Message{} |
| for _, r := range l.files { |
| fd := r.fd |
| pool := map[string]proto.Message{} |
| l.descriptorPool[fd] = pool |
| prefix := fd.GetPackage() |
| if prefix != "" { |
| prefix += "." |
| } |
| for _, md := range fd.MessageType { |
| if err := addMessageToPool(r, pool, prefix, md); err != nil { |
| return err |
| } |
| } |
| for _, fld := range fd.Extension { |
| if err := addFieldToPool(r, pool, prefix, fld); err != nil { |
| return err |
| } |
| } |
| for _, ed := range fd.EnumType { |
| if err := addEnumToPool(r, pool, prefix, ed); err != nil { |
| return err |
| } |
| } |
| for _, sd := range fd.Service { |
| if err := addServiceToPool(r, pool, prefix, sd); err != nil { |
| return err |
| } |
| } |
| } |
| // try putting everything into a single pool, to ensure there are no duplicates |
| // across files (e.g. same symbol, but declared in two different files) |
| type entry struct { |
| file string |
| msg proto.Message |
| } |
| pool := map[string]entry{} |
| for f, p := range l.descriptorPool { |
| for k, v := range p { |
| if e, ok := pool[k]; ok { |
| desc1 := e.msg |
| file1 := e.file |
| desc2 := v |
| file2 := f.GetName() |
| if file2 < file1 { |
| file1, file2 = file2, file1 |
| desc1, desc2 = desc2, desc1 |
| } |
| node := l.files[file2].nodes[desc2] |
| return ErrorWithSourcePos{Pos: node.start(), Underlying: fmt.Errorf("duplicate symbol %s: already defined as %s in %q", k, descriptorType(desc1), file1)} |
| } |
| pool[k] = entry{file: f.GetName(), msg: v} |
| } |
| } |
| |
| return nil |
| } |
| |
| func addMessageToPool(r *parseResult, pool map[string]proto.Message, prefix string, md *dpb.DescriptorProto) error { |
| fqn := prefix + md.GetName() |
| if err := addToPool(r, pool, fqn, md); err != nil { |
| return err |
| } |
| prefix = fqn + "." |
| for _, fld := range md.Field { |
| if err := addFieldToPool(r, pool, prefix, fld); err != nil { |
| return err |
| } |
| } |
| for _, fld := range md.Extension { |
| if err := addFieldToPool(r, pool, prefix, fld); err != nil { |
| return err |
| } |
| } |
| for _, nmd := range md.NestedType { |
| if err := addMessageToPool(r, pool, prefix, nmd); err != nil { |
| return err |
| } |
| } |
| for _, ed := range md.EnumType { |
| if err := addEnumToPool(r, pool, prefix, ed); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| func addFieldToPool(r *parseResult, pool map[string]proto.Message, prefix string, fld *dpb.FieldDescriptorProto) error { |
| fqn := prefix + fld.GetName() |
| return addToPool(r, pool, fqn, fld) |
| } |
| |
| func addEnumToPool(r *parseResult, pool map[string]proto.Message, prefix string, ed *dpb.EnumDescriptorProto) error { |
| fqn := prefix + ed.GetName() |
| if err := addToPool(r, pool, fqn, ed); err != nil { |
| return err |
| } |
| for _, evd := range ed.Value { |
| vfqn := fqn + "." + evd.GetName() |
| if err := addToPool(r, pool, vfqn, evd); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| func addServiceToPool(r *parseResult, pool map[string]proto.Message, prefix string, sd *dpb.ServiceDescriptorProto) error { |
| fqn := prefix + sd.GetName() |
| if err := addToPool(r, pool, fqn, sd); err != nil { |
| return err |
| } |
| for _, mtd := range sd.Method { |
| mfqn := fqn + "." + mtd.GetName() |
| if err := addToPool(r, pool, mfqn, mtd); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| func addToPool(r *parseResult, pool map[string]proto.Message, fqn string, dsc proto.Message) error { |
| if d, ok := pool[fqn]; ok { |
| node := r.nodes[dsc] |
| return ErrorWithSourcePos{Pos: node.start(), Underlying: fmt.Errorf("duplicate symbol %s: already defined as %s", fqn, descriptorType(d))} |
| } |
| pool[fqn] = dsc |
| return nil |
| } |
| |
| func descriptorType(m proto.Message) string { |
| switch m := m.(type) { |
| case *dpb.DescriptorProto: |
| return "message" |
| case *dpb.DescriptorProto_ExtensionRange: |
| return "extension range" |
| case *dpb.FieldDescriptorProto: |
| if m.GetExtendee() == "" { |
| return "field" |
| } else { |
| return "extension" |
| } |
| case *dpb.EnumDescriptorProto: |
| return "enum" |
| case *dpb.EnumValueDescriptorProto: |
| return "enum value" |
| case *dpb.ServiceDescriptorProto: |
| return "service" |
| case *dpb.MethodDescriptorProto: |
| return "method" |
| case *dpb.FileDescriptorProto: |
| return "file" |
| default: |
| // shouldn't be possible |
| return fmt.Sprintf("%T", m) |
| } |
| } |
| |
| func (l *linker) resolveReferences() error { |
| l.extensions = map[string]map[int32]string{} |
| for _, r := range l.files { |
| fd := r.fd |
| prefix := fd.GetPackage() |
| scopes := []scope{fileScope(fd, l)} |
| if prefix != "" { |
| prefix += "." |
| } |
| if fd.Options != nil { |
| if err := l.resolveOptions(r, fd, "file", fd.GetName(), proto.MessageName(fd.Options), fd.Options.UninterpretedOption, scopes); err != nil { |
| return err |
| } |
| } |
| for _, md := range fd.MessageType { |
| if err := l.resolveMessageTypes(r, fd, prefix, md, scopes); err != nil { |
| return err |
| } |
| } |
| for _, fld := range fd.Extension { |
| if err := l.resolveFieldTypes(r, fd, prefix, fld, scopes); err != nil { |
| return err |
| } |
| } |
| for _, ed := range fd.EnumType { |
| if err := l.resolveEnumTypes(r, fd, prefix, ed, scopes); err != nil { |
| return err |
| } |
| } |
| for _, sd := range fd.Service { |
| if err := l.resolveServiceTypes(r, fd, prefix, sd, scopes); err != nil { |
| return err |
| } |
| } |
| } |
| return nil |
| } |
| |
| func (l *linker) resolveEnumTypes(r *parseResult, fd *dpb.FileDescriptorProto, prefix string, ed *dpb.EnumDescriptorProto, scopes []scope) error { |
| enumFqn := prefix + ed.GetName() |
| if ed.Options != nil { |
| if err := l.resolveOptions(r, fd, "enum", enumFqn, proto.MessageName(ed.Options), ed.Options.UninterpretedOption, scopes); err != nil { |
| return err |
| } |
| } |
| for _, evd := range ed.Value { |
| if evd.Options != nil { |
| evFqn := enumFqn + "." + evd.GetName() |
| if err := l.resolveOptions(r, fd, "enum value", evFqn, proto.MessageName(evd.Options), evd.Options.UninterpretedOption, scopes); err != nil { |
| return err |
| } |
| } |
| } |
| return nil |
| } |
| |
| func (l *linker) resolveMessageTypes(r *parseResult, fd *dpb.FileDescriptorProto, prefix string, md *dpb.DescriptorProto, scopes []scope) error { |
| fqn := prefix + md.GetName() |
| scope := messageScope(fqn, isProto3(fd), l.descriptorPool[fd]) |
| scopes = append(scopes, scope) |
| prefix = fqn + "." |
| |
| if md.Options != nil { |
| if err := l.resolveOptions(r, fd, "message", fqn, proto.MessageName(md.Options), md.Options.UninterpretedOption, scopes); err != nil { |
| return err |
| } |
| } |
| |
| for _, nmd := range md.NestedType { |
| if err := l.resolveMessageTypes(r, fd, prefix, nmd, scopes); err != nil { |
| return err |
| } |
| } |
| for _, ned := range md.EnumType { |
| if err := l.resolveEnumTypes(r, fd, prefix, ned, scopes); err != nil { |
| return err |
| } |
| } |
| for _, fld := range md.Field { |
| if err := l.resolveFieldTypes(r, fd, prefix, fld, scopes); err != nil { |
| return err |
| } |
| } |
| for _, fld := range md.Extension { |
| if err := l.resolveFieldTypes(r, fd, prefix, fld, scopes); err != nil { |
| return err |
| } |
| } |
| for _, er := range md.ExtensionRange { |
| if er.Options != nil { |
| erName := fmt.Sprintf("%s:%d-%d", fqn, er.GetStart(), er.GetEnd()-1) |
| if err := l.resolveOptions(r, fd, "extension range", erName, proto.MessageName(er.Options), er.Options.UninterpretedOption, scopes); err != nil { |
| return err |
| } |
| } |
| } |
| return nil |
| } |
| |
| func (l *linker) resolveFieldTypes(r *parseResult, fd *dpb.FileDescriptorProto, prefix string, fld *dpb.FieldDescriptorProto, scopes []scope) error { |
| thisName := prefix + fld.GetName() |
| scope := fmt.Sprintf("field %s", thisName) |
| node := r.getFieldNode(fld) |
| elemType := "field" |
| if fld.GetExtendee() != "" { |
| fqn, dsc, _ := l.resolve(fd, fld.GetExtendee(), isMessage, scopes) |
| if dsc == nil { |
| return ErrorWithSourcePos{Pos: node.fieldExtendee().start(), Underlying: fmt.Errorf("unknown extendee type %s", fld.GetExtendee())} |
| } |
| extd, ok := dsc.(*dpb.DescriptorProto) |
| if !ok { |
| otherType := descriptorType(dsc) |
| return ErrorWithSourcePos{Pos: node.fieldExtendee().start(), Underlying: fmt.Errorf("extendee is invalid: %s is a %s, not a message", fqn, otherType)} |
| } |
| fld.Extendee = proto.String("." + fqn) |
| // make sure the tag number is in range |
| found := false |
| tag := fld.GetNumber() |
| for _, rng := range extd.ExtensionRange { |
| if tag >= rng.GetStart() && tag < rng.GetEnd() { |
| found = true |
| break |
| } |
| } |
| if !found { |
| return ErrorWithSourcePos{Pos: node.fieldTag().start(), Underlying: fmt.Errorf("%s: tag %d is not in valid range for extended type %s", scope, tag, fqn)} |
| } |
| // make sure tag is not a duplicate |
| usedExtTags := l.extensions[fqn] |
| if usedExtTags == nil { |
| usedExtTags = map[int32]string{} |
| l.extensions[fqn] = usedExtTags |
| } |
| if other := usedExtTags[fld.GetNumber()]; other != "" { |
| return ErrorWithSourcePos{Pos: node.fieldTag().start(), Underlying: fmt.Errorf("%s: duplicate extension: %s and %s are both using tag %d", scope, other, thisName, fld.GetNumber())} |
| } |
| usedExtTags[fld.GetNumber()] = thisName |
| elemType = "extension" |
| } |
| |
| if fld.Options != nil { |
| if err := l.resolveOptions(r, fd, elemType, thisName, proto.MessageName(fld.Options), fld.Options.UninterpretedOption, scopes); err != nil { |
| return err |
| } |
| } |
| |
| if fld.GetTypeName() == "" { |
| // scalar type; no further resolution required |
| return nil |
| } |
| |
| fqn, dsc, proto3 := l.resolve(fd, fld.GetTypeName(), isType, scopes) |
| if dsc == nil { |
| return ErrorWithSourcePos{Pos: node.fieldType().start(), Underlying: fmt.Errorf("%s: unknown type %s", scope, fld.GetTypeName())} |
| } |
| switch dsc := dsc.(type) { |
| case *dpb.DescriptorProto: |
| fld.TypeName = proto.String("." + fqn) |
| case *dpb.EnumDescriptorProto: |
| if fld.GetExtendee() == "" && isProto3(fd) && !proto3 { |
| // fields in a proto3 message cannot refer to proto2 enums |
| return ErrorWithSourcePos{Pos: node.fieldType().start(), Underlying: fmt.Errorf("%s: cannot use proto2 enum %s in a proto3 message", scope, fld.GetTypeName())} |
| } |
| fld.TypeName = proto.String("." + fqn) |
| // the type was tentatively set to message, but now we know it's actually an enum |
| fld.Type = dpb.FieldDescriptorProto_TYPE_ENUM.Enum() |
| default: |
| otherType := descriptorType(dsc) |
| return ErrorWithSourcePos{Pos: node.fieldType().start(), Underlying: fmt.Errorf("%s: invalid type: %s is a %s, not a message or enum", scope, fqn, otherType)} |
| } |
| return nil |
| } |
| |
| func (l *linker) resolveServiceTypes(r *parseResult, fd *dpb.FileDescriptorProto, prefix string, sd *dpb.ServiceDescriptorProto, scopes []scope) error { |
| thisName := prefix + sd.GetName() |
| if sd.Options != nil { |
| if err := l.resolveOptions(r, fd, "service", thisName, proto.MessageName(sd.Options), sd.Options.UninterpretedOption, scopes); err != nil { |
| return err |
| } |
| } |
| |
| for _, mtd := range sd.Method { |
| if mtd.Options != nil { |
| if err := l.resolveOptions(r, fd, "method", thisName+"."+mtd.GetName(), proto.MessageName(mtd.Options), mtd.Options.UninterpretedOption, scopes); err != nil { |
| return err |
| } |
| } |
| scope := fmt.Sprintf("method %s.%s", thisName, mtd.GetName()) |
| node := r.getMethodNode(mtd) |
| fqn, dsc, _ := l.resolve(fd, mtd.GetInputType(), isMessage, scopes) |
| if dsc == nil { |
| return ErrorWithSourcePos{Pos: node.getInputType().start(), Underlying: fmt.Errorf("%s: unknown request type %s", scope, mtd.GetInputType())} |
| } |
| if _, ok := dsc.(*dpb.DescriptorProto); !ok { |
| otherType := descriptorType(dsc) |
| return ErrorWithSourcePos{Pos: node.getInputType().start(), Underlying: fmt.Errorf("%s: invalid request type: %s is a %s, not a message", scope, fqn, otherType)} |
| } |
| mtd.InputType = proto.String("." + fqn) |
| |
| fqn, dsc, _ = l.resolve(fd, mtd.GetOutputType(), isMessage, scopes) |
| if dsc == nil { |
| return ErrorWithSourcePos{Pos: node.getOutputType().start(), Underlying: fmt.Errorf("%s: unknown response type %s", scope, mtd.GetOutputType())} |
| } |
| if _, ok := dsc.(*dpb.DescriptorProto); !ok { |
| otherType := descriptorType(dsc) |
| return ErrorWithSourcePos{Pos: node.getOutputType().start(), Underlying: fmt.Errorf("%s: invalid response type: %s is a %s, not a message", scope, fqn, otherType)} |
| } |
| mtd.OutputType = proto.String("." + fqn) |
| } |
| return nil |
| } |
| |
| func (l *linker) resolveOptions(r *parseResult, fd *dpb.FileDescriptorProto, elemType, elemName, optType string, opts []*dpb.UninterpretedOption, scopes []scope) error { |
| var scope string |
| if elemType != "file" { |
| scope = fmt.Sprintf("%s %s: ", elemType, elemName) |
| } |
| for _, opt := range opts { |
| for _, nm := range opt.Name { |
| if nm.GetIsExtension() { |
| node := r.getOptionNamePartNode(nm) |
| fqn, dsc, _ := l.resolve(fd, nm.GetNamePart(), isField, scopes) |
| if dsc == nil { |
| return ErrorWithSourcePos{Pos: node.start(), Underlying: fmt.Errorf("%sunknown extension %s", scope, nm.GetNamePart())} |
| } |
| if ext, ok := dsc.(*dpb.FieldDescriptorProto); !ok { |
| otherType := descriptorType(dsc) |
| return ErrorWithSourcePos{Pos: node.start(), Underlying: fmt.Errorf("%sinvalid extension: %s is a %s, not an extension", scope, nm.GetNamePart(), otherType)} |
| } else if ext.GetExtendee() == "" { |
| return ErrorWithSourcePos{Pos: node.start(), Underlying: fmt.Errorf("%sinvalid extension: %s is a field but not an extension", scope, nm.GetNamePart())} |
| } |
| nm.NamePart = proto.String("." + fqn) |
| } |
| } |
| } |
| return nil |
| } |
| |
| func (l *linker) resolve(fd *dpb.FileDescriptorProto, name string, allowed func(proto.Message) bool, scopes []scope) (fqn string, element proto.Message, proto3 bool) { |
| if strings.HasPrefix(name, ".") { |
| // already fully-qualified |
| d, proto3 := l.findSymbol(fd, name[1:], false, map[*dpb.FileDescriptorProto]struct{}{}) |
| if d != nil { |
| return name[1:], d, proto3 |
| } |
| } else { |
| // unqualified, so we look in the enclosing (last) scope first and move |
| // towards outermost (first) scope, trying to resolve the symbol |
| var bestGuess proto.Message |
| var bestGuessFqn string |
| var bestGuessProto3 bool |
| for i := len(scopes) - 1; i >= 0; i-- { |
| fqn, d, proto3 := scopes[i](name) |
| if d != nil { |
| if allowed(d) { |
| return fqn, d, proto3 |
| } else if bestGuess == nil { |
| bestGuess = d |
| bestGuessFqn = fqn |
| bestGuessProto3 = proto3 |
| } |
| } |
| } |
| // we return best guess, even though it was not an allowed kind of |
| // descriptor, so caller can print a better error message (e.g. |
| // indicating that the name was found but that it's the wrong type) |
| return bestGuessFqn, bestGuess, bestGuessProto3 |
| } |
| return "", nil, false |
| } |
| |
| func isField(m proto.Message) bool { |
| _, ok := m.(*dpb.FieldDescriptorProto) |
| return ok |
| } |
| |
| func isMessage(m proto.Message) bool { |
| _, ok := m.(*dpb.DescriptorProto) |
| return ok |
| } |
| |
| func isType(m proto.Message) bool { |
| switch m.(type) { |
| case *dpb.DescriptorProto, *dpb.EnumDescriptorProto: |
| return true |
| } |
| return false |
| } |
| |
| // scope represents a lexical scope in a proto file in which messages and enums |
| // can be declared. |
| type scope func(symbol string) (fqn string, element proto.Message, proto3 bool) |
| |
| func fileScope(fd *dpb.FileDescriptorProto, l *linker) scope { |
| // we search symbols in this file, but also symbols in other files that have |
| // the same package as this file or a "parent" package (in protobuf, |
| // packages are a hierarchy like C++ namespaces) |
| prefixes := internal.CreatePrefixList(fd.GetPackage()) |
| return func(name string) (string, proto.Message, bool) { |
| for _, prefix := range prefixes { |
| var n string |
| if prefix == "" { |
| n = name |
| } else { |
| n = prefix + "." + name |
| } |
| d, proto3 := l.findSymbol(fd, n, false, map[*dpb.FileDescriptorProto]struct{}{}) |
| if d != nil { |
| return n, d, proto3 |
| } |
| } |
| return "", nil, false |
| } |
| } |
| |
| func messageScope(messageName string, proto3 bool, filePool map[string]proto.Message) scope { |
| return func(name string) (string, proto.Message, bool) { |
| n := messageName + "." + name |
| if d, ok := filePool[n]; ok { |
| return n, d, proto3 |
| } |
| return "", nil, false |
| } |
| } |
| |
| func (l *linker) findSymbol(fd *dpb.FileDescriptorProto, name string, public bool, checked map[*dpb.FileDescriptorProto]struct{}) (element proto.Message, proto3 bool) { |
| if _, ok := checked[fd]; ok { |
| // already checked this one |
| return nil, false |
| } |
| checked[fd] = struct{}{} |
| d := l.descriptorPool[fd][name] |
| if d != nil { |
| return d, isProto3(fd) |
| } |
| |
| // When public = false, we are searching only directly imported symbols. But we |
| // also need to search transitive public imports due to semantics of public imports. |
| if public { |
| for _, depIndex := range fd.PublicDependency { |
| dep := fd.Dependency[depIndex] |
| depres := l.files[dep] |
| if depres == nil { |
| // we'll catch this error later |
| continue |
| } |
| if d, proto3 := l.findSymbol(depres.fd, name, true, checked); d != nil { |
| return d, proto3 |
| } |
| } |
| } else { |
| for _, dep := range fd.Dependency { |
| depres := l.files[dep] |
| if depres == nil { |
| // we'll catch this error later |
| continue |
| } |
| if d, proto3 := l.findSymbol(depres.fd, name, true, checked); d != nil { |
| return d, proto3 |
| } |
| } |
| } |
| |
| return nil, false |
| } |
| |
| func isProto3(fd *dpb.FileDescriptorProto) bool { |
| return fd.GetSyntax() == "proto3" |
| } |
| |
| func (l *linker) createdLinkedDescriptors() (map[string]*desc.FileDescriptor, error) { |
| names := make([]string, 0, len(l.files)) |
| for name := range l.files { |
| names = append(names, name) |
| } |
| sort.Strings(names) |
| linked := map[string]*desc.FileDescriptor{} |
| for _, name := range names { |
| if _, err := l.linkFile(name, nil, linked); err != nil { |
| return nil, err |
| } |
| } |
| return linked, nil |
| } |
| |
| func (l *linker) linkFile(name string, seen []string, linked map[string]*desc.FileDescriptor) (*desc.FileDescriptor, error) { |
| // check for import cycle |
| for _, s := range seen { |
| if name == s { |
| var msg bytes.Buffer |
| first := true |
| for _, s := range seen { |
| if first { |
| first = false |
| } else { |
| msg.WriteString(" -> ") |
| } |
| fmt.Fprintf(&msg, "%q", s) |
| } |
| fmt.Fprintf(&msg, " -> %q", name) |
| return nil, fmt.Errorf("cycle found in imports: %s", msg.String()) |
| } |
| } |
| seen = append(seen, name) |
| |
| if lfd, ok := linked[name]; ok { |
| // already linked |
| return lfd, nil |
| } |
| r := l.files[name] |
| if r == nil { |
| importer := seen[len(seen)-2] // len-1 is *this* file, before that is the one that imported it |
| return nil, fmt.Errorf("no descriptor found for %q, imported by %q", name, importer) |
| } |
| var deps []*desc.FileDescriptor |
| for _, dep := range r.fd.Dependency { |
| ldep, err := l.linkFile(dep, seen, linked) |
| if err != nil { |
| return nil, err |
| } |
| deps = append(deps, ldep) |
| } |
| lfd, err := desc.CreateFileDescriptor(r.fd, deps...) |
| if err != nil { |
| return nil, fmt.Errorf("error linking %q: %s", name, err) |
| } |
| linked[name] = lfd |
| return lfd, nil |
| } |