github.com/stackb/rules_proto@v0.0.0-20240221195024-5428336c51f1/pkg/protoc/depsresolver.go (about)

     1  package protoc
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"log"
     7  	"path"
     8  	"sort"
     9  	"strings"
    10  
    11  	"github.com/bazelbuild/bazel-gazelle/config"
    12  	"github.com/bazelbuild/bazel-gazelle/label"
    13  	"github.com/bazelbuild/bazel-gazelle/resolve"
    14  	"github.com/bazelbuild/bazel-gazelle/rule"
    15  )
    16  
    17  const (
    18  	ResolverLangName = "protobuf"
    19  	// ResolverImpLangPrivateKey stores the implementation language override.
    20  	ResolverImpLangPrivateKey = "_protobuf_imp_lang"
    21  	UnresolvedDepsPrivateKey  = "_unresolved_deps"
    22  )
    23  
    24  var (
    25  	errSkipImport = errors.New("self import")
    26  	errNotFound   = errors.New("rule not found")
    27  	ErrNoLabel    = errors.New("no label")
    28  )
    29  
    30  const debug = false
    31  
    32  type DepsResolver func(c *config.Config, ix *resolve.RuleIndex, r *rule.Rule, imports []string, from label.Label)
    33  
    34  // ResolveDepsAttr returns a function that implements the DepsResolver
    35  // interface.  This function resolves dependencies for a given rule attribute
    36  // name (typically "deps"); if there is a non-empty list of resolved
    37  // dependencies, the rule attribute will be overrwritten/modified by this
    38  // function (excluding duplicates, sorting applied).  The "from" argument
    39  // represents the rule being resolved (whose state is the *rule.Rule argument).
    40  // The "imports" list represents the list of imports that was originally
    41  // returned by the GenerateResponse.Imports (typically in via a private attr
    42  // GazelleImportsKey), and holds the values of all the import statements (e.g.
    43  // "google/protobuf/descriptor.proto") of the ProtoLibrary used to generate the
    44  // rule.  Special handling is provided for well-known types, which can be
    45  // excluded using the `excludeWkt` argument.  Actual resolution for an
    46  // individual import is delegated to the `resolveAnyKind` function.
    47  func ResolveDepsAttr(attrName string, excludeWkt bool) DepsResolver {
    48  	return func(c *config.Config, ix *resolve.RuleIndex, r *rule.Rule, imports []string, from label.Label) {
    49  		if debug {
    50  			log.Printf("%v (%s.%s): resolving %d imports: %v", from, r.Kind(), attrName, len(imports), imports)
    51  		}
    52  
    53  		existing := r.AttrStrings(attrName)
    54  		r.DelAttr(attrName)
    55  
    56  		depSet := make(map[string]bool)
    57  		for _, d := range existing {
    58  			depSet[d] = true
    59  		}
    60  
    61  		// unresolvedDeps is a mapping from the import string to the reason it
    62  		// was unresolved.  It is saved under 'UnresolvedDepsPrivateKey' if
    63  		// there were unresolved deps.  The value 'ErrNoLabel' is the most
    64  		// common case.
    65  		unresolvedDeps := make(map[string]error)
    66  
    67  		for _, imp := range imports {
    68  			if excludeWkt && strings.HasPrefix(imp, "google/protobuf/") {
    69  				continue
    70  			}
    71  
    72  			// determine the resolve kind
    73  			impLang := r.Kind()
    74  			if overrideImpLang, ok := r.PrivateAttr(ResolverImpLangPrivateKey).(string); ok {
    75  				impLang = overrideImpLang
    76  			}
    77  
    78  			if debug {
    79  				log.Println(from, "resolving:", imp, impLang)
    80  			}
    81  			l, err := resolveAnyKind(c, ix, ResolverLangName, impLang, imp, from)
    82  			if err == errSkipImport {
    83  				if debug {
    84  					log.Println(from, "skipped (errSkipImport):", imp)
    85  				}
    86  				continue
    87  			}
    88  			if err != nil {
    89  				log.Println(from, "ResolveDepsAttr error:", err)
    90  				unresolvedDeps[imp] = err
    91  				continue
    92  			}
    93  			if l == label.NoLabel {
    94  				if debug {
    95  					log.Println(from, "no label", imp)
    96  				}
    97  				unresolvedDeps[imp] = ErrNoLabel
    98  				continue
    99  			}
   100  
   101  			l = l.Rel(from.Repo, from.Pkg)
   102  			if debug {
   103  				log.Println(from, "resolved:", imp, "is provided by", l)
   104  			}
   105  			depSet[l.String()] = true
   106  		}
   107  
   108  		if len(depSet) > 0 {
   109  			deps := make([]string, 0, len(depSet))
   110  			for dep := range depSet {
   111  				deps = append(deps, dep)
   112  			}
   113  			sort.Strings(deps)
   114  			r.SetAttr(attrName, deps)
   115  			if debug {
   116  				log.Println(from, "resolved deps:", deps)
   117  			}
   118  		}
   119  
   120  		if len(unresolvedDeps) > 0 {
   121  			r.SetPrivateAttr(UnresolvedDepsPrivateKey, unresolvedDeps)
   122  		}
   123  	}
   124  }
   125  
   126  // resolveAnyKind answers the question "what bazel label provides a rule for the
   127  // given import?" (having the same rule kind as the given rule argument).  The
   128  // algorithm first consults the override list (configured either via gazelle
   129  // resolve directives, or via a YAML config).  If no override is found, the
   130  // RuleIndex is consulted, which contains all rules indexed by gazelle in the
   131  // generation phase.   If no match is found, return label.NoLabel.
   132  func resolveAnyKind(c *config.Config, ix *resolve.RuleIndex, lang, impLang, imp string, from label.Label) (label.Label, error) {
   133  	if l, ok := resolve.FindRuleWithOverride(c, resolve.ImportSpec{Lang: impLang, Imp: imp}, lang); ok {
   134  		// log.Println(from, "override hit:", l)
   135  		return l, nil
   136  	}
   137  	if l, err := resolveWithIndex(c, ix, lang, impLang, imp, from); err == nil || err == errSkipImport {
   138  		return l, err
   139  	} else if err != errNotFound {
   140  		return label.NoLabel, err
   141  	}
   142  	if debug {
   143  		log.Println(from, "fallback miss:", imp)
   144  	}
   145  	return label.NoLabel, nil
   146  }
   147  
   148  func resolveWithIndex(c *config.Config, ix *resolve.RuleIndex, lang, impLang, imp string, from label.Label) (label.Label, error) {
   149  	matches := ix.FindRulesByImportWithConfig(c, resolve.ImportSpec{Lang: impLang, Imp: imp}, lang)
   150  	if len(matches) == 0 {
   151  		// log.Println(from, "no matches:", imp)
   152  		return label.NoLabel, errNotFound
   153  	}
   154  	if len(matches) > 1 {
   155  		return label.NoLabel, fmt.Errorf("multiple rules (%s and %s) may be imported with %q from %s", matches[0].Label, matches[1].Label, imp, from)
   156  	}
   157  	if matches[0].IsSelfImport(from) || isSameImport(c, from, matches[0].Label) {
   158  		// log.Println(from, "self import:", imp)
   159  		return label.NoLabel, errSkipImport
   160  	}
   161  	// log.Println(from, "FindRulesByImportWithConfig first match:", imp, matches[0].Label)
   162  	return matches[0].Label, nil
   163  }
   164  
   165  // isSameImport returns true if the "from" and "to" labels are the same.  If the
   166  // "to" label is not a canonical label (having a fully-qualified repo name), a
   167  // canonical label is constructed for comparison using the config.RepoName.
   168  func isSameImport(c *config.Config, from, to label.Label) bool {
   169  	if from == to {
   170  		return true
   171  	}
   172  	if to.Repo != "" {
   173  		return false
   174  	}
   175  	canonical := label.New(c.RepoName, to.Pkg, to.Name)
   176  	return from == canonical
   177  }
   178  
   179  // StripRel removes the rel prefix from a filename (if has matching prefix)
   180  func StripRel(rel string, filename string) string {
   181  	if !strings.HasPrefix(filename, rel) {
   182  		return filename
   183  	}
   184  	filename = filename[len(rel):]
   185  	return strings.TrimPrefix(filename, "/")
   186  }
   187  
   188  // ProtoLibraryImportSpecsForKind generates an ImportSpec for each file in the
   189  // set of given proto_library.
   190  func ProtoLibraryImportSpecsForKind(kind string, libs ...ProtoLibrary) []resolve.ImportSpec {
   191  	specs := make([]resolve.ImportSpec, 0)
   192  	for _, lib := range libs {
   193  		specs = append(specs, ProtoFilesImportSpecsForKind(kind, lib.Files())...)
   194  	}
   195  
   196  	return specs
   197  }
   198  
   199  // ProtoLibraryImportSpecsForKind generates an ImportSpec for each file in the
   200  // set of given proto_library.
   201  func ProtoFilesImportSpecsForKind(kind string, files []*File) []resolve.ImportSpec {
   202  	specs := make([]resolve.ImportSpec, 0)
   203  	for _, file := range files {
   204  		imp := path.Join(file.Dir, file.Basename)
   205  		spec := resolve.ImportSpec{Lang: kind, Imp: imp}
   206  		specs = append(specs, spec)
   207  	}
   208  	return specs
   209  }