github.com/bazelbuild/bazel-gazelle@v0.36.1-0.20240520142334-61b277ba6fed/language/proto/generate.go (about)

     1  /* Copyright 2018 The Bazel Authors. All rights reserved.
     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  
    16  package proto
    17  
    18  import (
    19  	"fmt"
    20  	"log"
    21  	"path"
    22  	"sort"
    23  	"strings"
    24  
    25  	"github.com/bazelbuild/bazel-gazelle/config"
    26  	"github.com/bazelbuild/bazel-gazelle/language"
    27  	"github.com/bazelbuild/bazel-gazelle/pathtools"
    28  	"github.com/bazelbuild/bazel-gazelle/rule"
    29  )
    30  
    31  func (*protoLang) GenerateRules(args language.GenerateArgs) language.GenerateResult {
    32  	c := args.Config
    33  	pc := GetProtoConfig(c)
    34  	if !pc.Mode.ShouldGenerateRules() {
    35  		// Don't create or delete proto rules in this mode. Any existing rules
    36  		// are likely hand-written.
    37  		return language.GenerateResult{}
    38  	}
    39  
    40  	var regularProtoFiles []string
    41  	for _, name := range args.RegularFiles {
    42  		if strings.HasSuffix(name, ".proto") {
    43  			regularProtoFiles = append(regularProtoFiles, name)
    44  		}
    45  	}
    46  
    47  	// Some of the generated files may have been consumed by other rules
    48  	consumedFileSet := make(map[string]bool)
    49  	for _, r := range args.OtherGen {
    50  		if r.Kind() != "proto_library" {
    51  			continue
    52  		}
    53  		for _, f := range r.AttrStrings("srcs") {
    54  			consumedFileSet[f] = true
    55  		}
    56  	}
    57  
    58  	// genProtoFilesNotConsumed represents only not consumed generated files.
    59  	// genProtoFiles represents all generated files.
    60  	// This is required for not generating empty rules for consumed generated
    61  	// files.
    62  	var genProtoFiles, genProtoFilesNotConsumed []string
    63  	for _, name := range args.GenFiles {
    64  		if strings.HasSuffix(name, ".proto") {
    65  			genProtoFiles = append(genProtoFiles, name)
    66  			if !consumedFileSet[name] {
    67  				genProtoFilesNotConsumed = append(genProtoFilesNotConsumed, name)
    68  			}
    69  		}
    70  	}
    71  	pkgs := buildPackages(pc, args.Dir, args.Rel, regularProtoFiles, genProtoFilesNotConsumed)
    72  	shouldSetVisibility := args.File == nil || !args.File.HasDefaultVisibility()
    73  	var res language.GenerateResult
    74  	for _, pkg := range pkgs {
    75  		r := generateProto(pc, args.Rel, pkg, shouldSetVisibility)
    76  		if r.IsEmpty(protoKinds[r.Kind()]) {
    77  			res.Empty = append(res.Empty, r)
    78  		} else {
    79  			res.Gen = append(res.Gen, r)
    80  		}
    81  	}
    82  	sort.SliceStable(res.Gen, func(i, j int) bool {
    83  		return res.Gen[i].Name() < res.Gen[j].Name()
    84  	})
    85  	res.Imports = make([]interface{}, len(res.Gen))
    86  	for i, r := range res.Gen {
    87  		res.Imports[i] = r.PrivateAttr(config.GazelleImportsKey)
    88  	}
    89  	res.Empty = append(res.Empty, generateEmpty(args.File, regularProtoFiles, genProtoFiles)...)
    90  	return res
    91  }
    92  
    93  // RuleName returns a name for a proto_library derived from the given strings.
    94  // For each string, RuleName will look for a non-empty suffix of identifier
    95  // characters and then append "_proto" to that.
    96  func RuleName(names ...string) string {
    97  	base := "root"
    98  	for _, name := range names {
    99  		notIdent := func(c rune) bool {
   100  			return !('A' <= c && c <= 'Z' ||
   101  				'a' <= c && c <= 'z' ||
   102  				'0' <= c && c <= '9' ||
   103  				c == '_')
   104  		}
   105  		if i := strings.LastIndexFunc(name, notIdent); i >= 0 {
   106  			name = name[i+1:]
   107  		}
   108  		if name != "" {
   109  			base = name
   110  			break
   111  		}
   112  	}
   113  	return base + "_proto"
   114  }
   115  
   116  // buildPackage extracts metadata from the .proto files in a directory and
   117  // constructs possibly several packages, then selects a package to generate
   118  // a proto_library rule for.
   119  func buildPackages(pc *ProtoConfig, dir, rel string, protoFiles, genFiles []string) []*Package {
   120  	packageMap := make(map[string]*Package)
   121  	for _, name := range protoFiles {
   122  		info := protoFileInfo(dir, name)
   123  		key := info.PackageName
   124  
   125  		if pc.Mode == FileMode {
   126  			key = strings.TrimSuffix(name, ".proto")
   127  		} else if pc.groupOption != "" { // implicitly PackageMode
   128  			for _, opt := range info.Options {
   129  				if opt.Key == pc.groupOption {
   130  					key = opt.Value
   131  					break
   132  				}
   133  			}
   134  		}
   135  
   136  		if packageMap[key] == nil {
   137  			packageMap[key] = newPackage(info.PackageName)
   138  		}
   139  		packageMap[key].addFile(info)
   140  		if key != info.PackageName {
   141  			packageMap[key].RuleName = key
   142  		}
   143  	}
   144  
   145  	switch pc.Mode {
   146  	case DefaultMode:
   147  		pkg, err := selectPackage(dir, rel, packageMap)
   148  		if err != nil {
   149  			log.Print(err)
   150  		}
   151  		if pkg == nil {
   152  			return nil // empty rule created in generateEmpty
   153  		}
   154  		for _, name := range genFiles {
   155  			pkg.addGenFile(dir, name)
   156  		}
   157  		return []*Package{pkg}
   158  
   159  	case PackageMode, FileMode:
   160  		pkgs := make([]*Package, 0, len(packageMap))
   161  		for _, pkg := range packageMap {
   162  			pkgs = append(pkgs, pkg)
   163  		}
   164  		return pkgs
   165  
   166  	default:
   167  		return nil
   168  	}
   169  }
   170  
   171  // selectPackage chooses a package to generate rules for.
   172  func selectPackage(dir, rel string, packageMap map[string]*Package) (*Package, error) {
   173  	if len(packageMap) == 0 {
   174  		return nil, nil
   175  	}
   176  	if len(packageMap) == 1 {
   177  		for _, pkg := range packageMap {
   178  			return pkg, nil
   179  		}
   180  	}
   181  	defaultPackageName := strings.Replace(rel, "/", "_", -1)
   182  	for _, pkg := range packageMap {
   183  		if pkgName := goPackageName(pkg); pkgName != "" && pkgName == defaultPackageName {
   184  			return pkg, nil
   185  		}
   186  	}
   187  	return nil, fmt.Errorf("%s: directory contains multiple proto packages. Gazelle can only generate a proto_library for one package.", dir)
   188  }
   189  
   190  // goPackageName guesses the identifier in package declarations at the top of
   191  // the .pb.go files that will be generated for this package. "" is returned
   192  // if the package name cannot be determined.
   193  //
   194  // TODO(jayconrod): remove all Go-specific functionality. This is here
   195  // temporarily for compatibility.
   196  func goPackageName(pkg *Package) string {
   197  	if opt, ok := pkg.Options["go_package"]; ok {
   198  		if i := strings.IndexByte(opt, ';'); i >= 0 {
   199  			return opt[i+1:]
   200  		} else if i := strings.LastIndexByte(opt, '/'); i >= 0 {
   201  			return opt[i+1:]
   202  		} else {
   203  			return opt
   204  		}
   205  	}
   206  	if pkg.Name != "" {
   207  		return strings.Replace(pkg.Name, ".", "_", -1)
   208  	}
   209  	if len(pkg.Files) == 1 {
   210  		for s := range pkg.Files {
   211  			return strings.TrimSuffix(s, ".proto")
   212  		}
   213  	}
   214  	return ""
   215  }
   216  
   217  // generateProto creates a new proto_library rule for a package. The rule may
   218  // be empty if there are no sources.
   219  func generateProto(pc *ProtoConfig, rel string, pkg *Package, shouldSetVisibility bool) *rule.Rule {
   220  	var name string
   221  	if pc.Mode == DefaultMode {
   222  		name = RuleName(goPackageName(pkg), pc.GoPrefix, rel)
   223  	} else {
   224  		name = RuleName(pkg.RuleName, pkg.Name, rel)
   225  	}
   226  	r := rule.NewRule("proto_library", name)
   227  	srcs := make([]string, 0, len(pkg.Files))
   228  	for f := range pkg.Files {
   229  		srcs = append(srcs, f)
   230  	}
   231  	sort.Strings(srcs)
   232  	if len(srcs) > 0 {
   233  		r.SetAttr("srcs", srcs)
   234  	}
   235  	r.SetPrivateAttr(PackageKey, *pkg)
   236  	imports := make([]string, 0, len(pkg.Imports))
   237  	for i := range pkg.Imports {
   238  		// If the proto import is a self import (an import between the same package), skip it
   239  		if _, ok := pkg.Files[path.Base(i)]; ok && getPrefix(pc, path.Dir(i)) == getPrefix(pc, rel) {
   240  			delete(pkg.Imports, i)
   241  			continue
   242  		}
   243  		imports = append(imports, i)
   244  	}
   245  	sort.Strings(imports)
   246  	// NOTE: This attribute should not be used outside this extension. It's still
   247  	// convenient for testing though.
   248  	r.SetPrivateAttr(config.GazelleImportsKey, imports)
   249  	for k, v := range pkg.Options {
   250  		r.SetPrivateAttr(k, v)
   251  	}
   252  	if shouldSetVisibility {
   253  		vis := rule.CheckInternalVisibility(rel, "//visibility:public")
   254  		r.SetAttr("visibility", []string{vis})
   255  	}
   256  	if pc.StripImportPrefix != "" {
   257  		r.SetAttr("strip_import_prefix", pc.StripImportPrefix)
   258  	}
   259  	if pc.ImportPrefix != "" {
   260  		r.SetAttr("import_prefix", pc.ImportPrefix)
   261  	}
   262  	return r
   263  }
   264  
   265  func getPrefix(pc *ProtoConfig, rel string) string {
   266  	prefix := rel
   267  	if strings.HasPrefix(pc.StripImportPrefix, "/") {
   268  		prefix = pathtools.TrimPrefix(rel, pc.StripImportPrefix[len("/"):])
   269  	} else if pc.StripImportPrefix != "" {
   270  		prefix = pathtools.TrimPrefix(rel, path.Join(rel, pc.StripImportPrefix))
   271  	}
   272  	if pc.ImportPrefix != "" {
   273  		return path.Join(pc.ImportPrefix, prefix)
   274  	}
   275  	return prefix
   276  }
   277  
   278  // generateEmpty generates a list of proto_library rules that may be deleted.
   279  // This is generated from existing proto_library rules with srcs lists that
   280  // don't match any static or generated files.
   281  func generateEmpty(f *rule.File, regularFiles, genFiles []string) []*rule.Rule {
   282  	if f == nil {
   283  		return nil
   284  	}
   285  	knownFiles := make(map[string]bool)
   286  	for _, f := range regularFiles {
   287  		knownFiles[f] = true
   288  	}
   289  	for _, f := range genFiles {
   290  		knownFiles[f] = true
   291  	}
   292  	var empty []*rule.Rule
   293  outer:
   294  	for _, r := range f.Rules {
   295  		if r.Kind() != "proto_library" {
   296  			continue
   297  		}
   298  		srcs := r.AttrStrings("srcs")
   299  		if len(srcs) == 0 && r.Attr("srcs") != nil {
   300  			// srcs is not a string list; leave it alone
   301  			continue
   302  		}
   303  		for _, src := range r.AttrStrings("srcs") {
   304  			if knownFiles[src] {
   305  				continue outer
   306  			}
   307  		}
   308  		empty = append(empty, rule.NewRule("proto_library", r.Name()))
   309  	}
   310  	return empty
   311  }