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