github.com/wolfd/bazel-gazelle@v0.14.0/internal/language/go/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 golang
    17  
    18  import (
    19  	"fmt"
    20  	"go/build"
    21  	"log"
    22  	"path"
    23  	"path/filepath"
    24  	"sort"
    25  	"strings"
    26  	"sync"
    27  
    28  	"github.com/bazelbuild/bazel-gazelle/internal/config"
    29  	"github.com/bazelbuild/bazel-gazelle/internal/language/proto"
    30  	"github.com/bazelbuild/bazel-gazelle/internal/pathtools"
    31  	"github.com/bazelbuild/bazel-gazelle/internal/rule"
    32  )
    33  
    34  func (gl *goLang) GenerateRules(c *config.Config, dir, rel string, f *rule.File, subdirs, regularFiles, genFiles []string, otherEmpty, otherGen []*rule.Rule) (empty, gen []*rule.Rule) {
    35  	// Extract information about proto files. We need this to exclude .pb.go
    36  	// files and generate go_proto_library rules.
    37  	gc := getGoConfig(c)
    38  	pc := proto.GetProtoConfig(c)
    39  	var protoRuleNames []string
    40  	protoPackages := make(map[string]proto.Package)
    41  	protoFileInfo := make(map[string]proto.FileInfo)
    42  	for _, r := range otherGen {
    43  		if r.Kind() != "proto_library" {
    44  			continue
    45  		}
    46  		pkg := r.PrivateAttr(proto.PackageKey).(proto.Package)
    47  		protoPackages[r.Name()] = pkg
    48  		for name, info := range pkg.Files {
    49  			protoFileInfo[name] = info
    50  		}
    51  		protoRuleNames = append(protoRuleNames, r.Name())
    52  	}
    53  	sort.Strings(protoRuleNames)
    54  	var emptyProtoRuleNames []string
    55  	for _, r := range otherEmpty {
    56  		if r.Kind() == "proto_library" {
    57  			emptyProtoRuleNames = append(emptyProtoRuleNames, r.Name())
    58  		}
    59  	}
    60  
    61  	// If proto rule generation is enabled, exclude .pb.go files that correspond
    62  	// to any .proto files present.
    63  	if !pc.Mode.ShouldIncludePregeneratedFiles() {
    64  		keep := func(f string) bool {
    65  			if strings.HasSuffix(f, ".pb.go") {
    66  				_, ok := protoFileInfo[strings.TrimSuffix(f, ".pb.go")+".proto"]
    67  				return !ok
    68  			}
    69  			return true
    70  		}
    71  		filterFiles(&regularFiles, keep)
    72  		filterFiles(&genFiles, keep)
    73  	}
    74  
    75  	// Split regular files into files which can determine the package name and
    76  	// import path and other files.
    77  	var goFiles, otherFiles []string
    78  	for _, f := range regularFiles {
    79  		if strings.HasSuffix(f, ".go") {
    80  			goFiles = append(goFiles, f)
    81  		} else {
    82  			otherFiles = append(otherFiles, f)
    83  		}
    84  	}
    85  
    86  	// Look for a subdirectory named testdata. Only treat it as data if it does
    87  	// not contain a buildable package.
    88  	var hasTestdata bool
    89  	for _, sub := range subdirs {
    90  		if sub == "testdata" {
    91  			hasTestdata = !gl.goPkgRels[path.Join(rel, "testdata")]
    92  			break
    93  		}
    94  	}
    95  
    96  	// Build a set of packages from files in this directory.
    97  	goPackageMap, goFilesWithUnknownPackage := buildPackages(c, dir, rel, goFiles, hasTestdata)
    98  
    99  	// Select a package to generate rules for. If there is no package, create
   100  	// an empty package so we can generate empty rules.
   101  	var protoName string
   102  	pkg, err := selectPackage(c, dir, goPackageMap)
   103  	if err != nil {
   104  		if _, ok := err.(*build.NoGoError); ok {
   105  			if len(protoPackages) == 1 {
   106  				for name, ppkg := range protoPackages {
   107  					pkg = &goPackage{
   108  						name:       goProtoPackageName(ppkg),
   109  						importPath: goProtoImportPath(gc, ppkg, rel),
   110  						proto:      protoTargetFromProtoPackage(name, ppkg),
   111  					}
   112  					protoName = name
   113  					break
   114  				}
   115  			} else {
   116  				pkg = emptyPackage(c, dir, rel)
   117  			}
   118  		} else {
   119  			log.Print(err)
   120  		}
   121  	}
   122  
   123  	// Try to link the selected package with a proto package.
   124  	if pkg != nil {
   125  		if pkg.importPath == "" {
   126  			if err := pkg.inferImportPath(c); err != nil && pkg.firstGoFile() != "" {
   127  				inferImportPathErrorOnce.Do(func() { log.Print(err) })
   128  			}
   129  		}
   130  		for _, name := range protoRuleNames {
   131  			ppkg := protoPackages[name]
   132  			if pkg.importPath == goProtoImportPath(gc, ppkg, rel) {
   133  				protoName = name
   134  				pkg.proto = protoTargetFromProtoPackage(name, ppkg)
   135  				break
   136  			}
   137  		}
   138  	}
   139  
   140  	// Generate rules for proto packages. These should come before the other
   141  	// Go rules.
   142  	g := newGenerator(c, f, rel)
   143  	var rules []*rule.Rule
   144  	var protoEmbed string
   145  	for _, name := range protoRuleNames {
   146  		ppkg := protoPackages[name]
   147  		var rs []*rule.Rule
   148  		if name == protoName {
   149  			protoEmbed, rs = g.generateProto(pc.Mode, pkg.proto, pkg.importPath)
   150  		} else {
   151  			target := protoTargetFromProtoPackage(name, ppkg)
   152  			importPath := goProtoImportPath(gc, ppkg, rel)
   153  			_, rs = g.generateProto(pc.Mode, target, importPath)
   154  		}
   155  		rules = append(rules, rs...)
   156  	}
   157  	for _, name := range emptyProtoRuleNames {
   158  		goProtoName := strings.TrimSuffix(name, "_proto") + "_go_proto"
   159  		empty = append(empty, rule.NewRule("go_proto_library", goProtoName))
   160  	}
   161  	if pkg != nil && pc.Mode == proto.PackageMode && pkg.firstGoFile() == "" {
   162  		// In proto package mode, don't generate a go_library embedding a
   163  		// go_proto_library unless there are actually go files.
   164  		protoEmbed = ""
   165  	}
   166  
   167  	// Complete the Go package and generate rules for that.
   168  	if pkg != nil {
   169  		// Add files with unknown packages. This happens when there are parse
   170  		// or I/O errors. We should keep the file in the srcs list and let the
   171  		// compiler deal with the error.
   172  		cgo := pkg.haveCgo()
   173  		for _, info := range goFilesWithUnknownPackage {
   174  			if err := pkg.addFile(c, info, cgo); err != nil {
   175  				log.Print(err)
   176  			}
   177  		}
   178  
   179  		// Process the other static files.
   180  		for _, file := range otherFiles {
   181  			info := otherFileInfo(filepath.Join(dir, file))
   182  			if err := pkg.addFile(c, info, cgo); err != nil {
   183  				log.Print(err)
   184  			}
   185  		}
   186  
   187  		// Process generated files. Note that generated files may have the same names
   188  		// as static files. Bazel will use the generated files, but we will look at
   189  		// the content of static files, assuming they will be the same.
   190  		regularFileSet := make(map[string]bool)
   191  		for _, f := range regularFiles {
   192  			regularFileSet[f] = true
   193  		}
   194  		for _, f := range genFiles {
   195  			if regularFileSet[f] {
   196  				continue
   197  			}
   198  			info := fileNameInfo(filepath.Join(dir, f))
   199  			if err := pkg.addFile(c, info, cgo); err != nil {
   200  				log.Print(err)
   201  			}
   202  		}
   203  
   204  		// Generate Go rules.
   205  		if protoName == "" {
   206  			// Empty proto rules for deletion.
   207  			_, rs := g.generateProto(pc.Mode, pkg.proto, pkg.importPath)
   208  			rules = append(rules, rs...)
   209  		}
   210  		lib := g.generateLib(pkg, protoEmbed)
   211  		var libName string
   212  		if !lib.IsEmpty(goKinds[lib.Kind()]) {
   213  			libName = lib.Name()
   214  		}
   215  		rules = append(rules, lib)
   216  		rules = append(rules,
   217  			g.generateBin(pkg, libName),
   218  			g.generateTest(pkg, libName))
   219  	}
   220  
   221  	for _, r := range rules {
   222  		if r.IsEmpty(goKinds[r.Kind()]) {
   223  			empty = append(empty, r)
   224  		} else {
   225  			gen = append(gen, r)
   226  		}
   227  	}
   228  
   229  	if f != nil || len(gen) > 0 {
   230  		gl.goPkgRels[rel] = true
   231  	} else {
   232  		for _, sub := range subdirs {
   233  			if gl.goPkgRels[path.Join(rel, sub)] {
   234  				gl.goPkgRels[rel] = true
   235  				break
   236  			}
   237  		}
   238  	}
   239  
   240  	return empty, gen
   241  }
   242  
   243  func filterFiles(files *[]string, pred func(string) bool) {
   244  	w := 0
   245  	for r := 0; r < len(*files); r++ {
   246  		f := (*files)[r]
   247  		if pred(f) {
   248  			(*files)[w] = f
   249  			w++
   250  		}
   251  	}
   252  	*files = (*files)[:w]
   253  }
   254  
   255  func buildPackages(c *config.Config, dir, rel string, goFiles []string, hasTestdata bool) (packageMap map[string]*goPackage, goFilesWithUnknownPackage []fileInfo) {
   256  	// Process .go and .proto files first, since these determine the package name.
   257  	packageMap = make(map[string]*goPackage)
   258  	for _, f := range goFiles {
   259  		path := filepath.Join(dir, f)
   260  		info := goFileInfo(path, rel)
   261  		if info.packageName == "" {
   262  			goFilesWithUnknownPackage = append(goFilesWithUnknownPackage, info)
   263  			continue
   264  		}
   265  		if info.packageName == "documentation" {
   266  			// go/build ignores this package
   267  			continue
   268  		}
   269  
   270  		if _, ok := packageMap[info.packageName]; !ok {
   271  			packageMap[info.packageName] = &goPackage{
   272  				name:        info.packageName,
   273  				dir:         dir,
   274  				rel:         rel,
   275  				hasTestdata: hasTestdata,
   276  			}
   277  		}
   278  		if err := packageMap[info.packageName].addFile(c, info, false); err != nil {
   279  			log.Print(err)
   280  		}
   281  	}
   282  	return packageMap, goFilesWithUnknownPackage
   283  }
   284  
   285  var inferImportPathErrorOnce sync.Once
   286  
   287  // selectPackages selects one Go packages out of the buildable packages found
   288  // in a directory. If multiple packages are found, it returns the package
   289  // whose name matches the directory if such a package exists.
   290  func selectPackage(c *config.Config, dir string, packageMap map[string]*goPackage) (*goPackage, error) {
   291  	buildablePackages := make(map[string]*goPackage)
   292  	for name, pkg := range packageMap {
   293  		if pkg.isBuildable(c) {
   294  			buildablePackages[name] = pkg
   295  		}
   296  	}
   297  
   298  	if len(buildablePackages) == 0 {
   299  		return nil, &build.NoGoError{Dir: dir}
   300  	}
   301  
   302  	if len(buildablePackages) == 1 {
   303  		for _, pkg := range buildablePackages {
   304  			return pkg, nil
   305  		}
   306  	}
   307  
   308  	if pkg, ok := buildablePackages[defaultPackageName(c, dir)]; ok {
   309  		return pkg, nil
   310  	}
   311  
   312  	err := &build.MultiplePackageError{Dir: dir}
   313  	for name, pkg := range buildablePackages {
   314  		// Add the first file for each package for the error message.
   315  		// Error() method expects these lists to be the same length. File
   316  		// lists must be non-empty. These lists are only created by
   317  		// buildPackage for packages with .go files present.
   318  		err.Packages = append(err.Packages, name)
   319  		err.Files = append(err.Files, pkg.firstGoFile())
   320  	}
   321  	return nil, err
   322  }
   323  
   324  func emptyPackage(c *config.Config, dir, rel string) *goPackage {
   325  	pkg := &goPackage{
   326  		name: defaultPackageName(c, dir),
   327  		dir:  dir,
   328  		rel:  rel,
   329  	}
   330  	pkg.inferImportPath(c)
   331  	return pkg
   332  }
   333  
   334  func defaultPackageName(c *config.Config, rel string) string {
   335  	gc := getGoConfig(c)
   336  	return pathtools.RelBaseName(rel, gc.prefix, "")
   337  }
   338  
   339  // hasDefaultVisibility returns whether oldFile contains a "package" rule with
   340  // a "default_visibility" attribute. Rules generated by Gazelle should not
   341  // have their own visibility attributes if this is the case.
   342  func hasDefaultVisibility(oldFile *rule.File) bool {
   343  	for _, r := range oldFile.Rules {
   344  		if r.Kind() == "package" && r.Attr("default_visibility") != nil {
   345  			return true
   346  		}
   347  	}
   348  	return false
   349  }
   350  
   351  // checkInternalVisibility overrides the given visibility if the package is
   352  // internal.
   353  func checkInternalVisibility(rel, visibility string) string {
   354  	if i := strings.LastIndex(rel, "/internal/"); i >= 0 {
   355  		visibility = fmt.Sprintf("//%s:__subpackages__", rel[:i])
   356  	} else if strings.HasPrefix(rel, "internal/") {
   357  		visibility = "//:__subpackages__"
   358  	}
   359  	return visibility
   360  }
   361  
   362  type generator struct {
   363  	c                   *config.Config
   364  	rel                 string
   365  	shouldSetVisibility bool
   366  }
   367  
   368  func newGenerator(c *config.Config, f *rule.File, rel string) *generator {
   369  	shouldSetVisibility := f == nil || !hasDefaultVisibility(f)
   370  	return &generator{c: c, rel: rel, shouldSetVisibility: shouldSetVisibility}
   371  }
   372  
   373  func (g *generator) generateProto(mode proto.Mode, target protoTarget, importPath string) (string, []*rule.Rule) {
   374  	if !mode.ShouldGenerateRules() && mode != proto.LegacyMode {
   375  		// Don't create or delete proto rules in this mode. Any existing rules
   376  		// are likely hand-written.
   377  		return "", nil
   378  	}
   379  
   380  	filegroupName := config.DefaultProtosName
   381  	protoName := target.name
   382  	if protoName == "" {
   383  		importPath := inferImportPath(getGoConfig(g.c), g.rel)
   384  		protoName = proto.RuleName(importPath)
   385  	}
   386  	goProtoName := strings.TrimSuffix(protoName, "_proto") + "_go_proto"
   387  	visibility := []string{checkInternalVisibility(g.rel, "//visibility:public")}
   388  
   389  	if mode == proto.LegacyMode {
   390  		filegroup := rule.NewRule("filegroup", filegroupName)
   391  		if target.sources.isEmpty() {
   392  			return "", []*rule.Rule{filegroup}
   393  		}
   394  		filegroup.SetAttr("srcs", target.sources.build())
   395  		if g.shouldSetVisibility {
   396  			filegroup.SetAttr("visibility", visibility)
   397  		}
   398  		return "", []*rule.Rule{filegroup}
   399  	}
   400  
   401  	if target.sources.isEmpty() {
   402  		return "", []*rule.Rule{
   403  			rule.NewRule("filegroup", filegroupName),
   404  			rule.NewRule("go_proto_library", goProtoName),
   405  		}
   406  	}
   407  
   408  	goProtoLibrary := rule.NewRule("go_proto_library", goProtoName)
   409  	goProtoLibrary.SetAttr("proto", ":"+protoName)
   410  	g.setImportAttrs(goProtoLibrary, importPath)
   411  	if target.hasServices {
   412  		goProtoLibrary.SetAttr("compilers", []string{"@io_bazel_rules_go//proto:go_grpc"})
   413  	}
   414  	if g.shouldSetVisibility {
   415  		goProtoLibrary.SetAttr("visibility", visibility)
   416  	}
   417  	goProtoLibrary.SetPrivateAttr(config.GazelleImportsKey, target.imports.build())
   418  	return goProtoName, []*rule.Rule{goProtoLibrary}
   419  }
   420  
   421  func (g *generator) generateLib(pkg *goPackage, embed string) *rule.Rule {
   422  	goLibrary := rule.NewRule("go_library", config.DefaultLibName)
   423  	if !pkg.library.sources.hasGo() && embed == "" {
   424  		return goLibrary // empty
   425  	}
   426  	var visibility string
   427  	if pkg.isCommand() {
   428  		// Libraries made for a go_binary should not be exposed to the public.
   429  		visibility = "//visibility:private"
   430  	} else {
   431  		visibility = checkInternalVisibility(pkg.rel, "//visibility:public")
   432  	}
   433  	g.setCommonAttrs(goLibrary, pkg.rel, visibility, pkg.library, embed)
   434  	g.setImportAttrs(goLibrary, pkg.importPath)
   435  	return goLibrary
   436  }
   437  
   438  func (g *generator) generateBin(pkg *goPackage, library string) *rule.Rule {
   439  	name := pathtools.RelBaseName(pkg.rel, getGoConfig(g.c).prefix, g.c.RepoRoot)
   440  	goBinary := rule.NewRule("go_binary", name)
   441  	if !pkg.isCommand() || pkg.binary.sources.isEmpty() && library == "" {
   442  		return goBinary // empty
   443  	}
   444  	visibility := checkInternalVisibility(pkg.rel, "//visibility:public")
   445  	g.setCommonAttrs(goBinary, pkg.rel, visibility, pkg.binary, library)
   446  	return goBinary
   447  }
   448  
   449  func (g *generator) generateTest(pkg *goPackage, library string) *rule.Rule {
   450  	goTest := rule.NewRule("go_test", config.DefaultTestName)
   451  	if !pkg.test.sources.hasGo() {
   452  		return goTest // empty
   453  	}
   454  	g.setCommonAttrs(goTest, pkg.rel, "", pkg.test, library)
   455  	if pkg.hasTestdata {
   456  		goTest.SetAttr("data", rule.GlobValue{Patterns: []string{"testdata/**"}})
   457  	}
   458  	return goTest
   459  }
   460  
   461  func (g *generator) setCommonAttrs(r *rule.Rule, pkgRel, visibility string, target goTarget, embed string) {
   462  	if !target.sources.isEmpty() {
   463  		r.SetAttr("srcs", target.sources.buildFlat())
   464  	}
   465  	if target.cgo {
   466  		r.SetAttr("cgo", true)
   467  	}
   468  	if !target.clinkopts.isEmpty() {
   469  		r.SetAttr("clinkopts", g.options(target.clinkopts.build(), pkgRel))
   470  	}
   471  	if !target.copts.isEmpty() {
   472  		r.SetAttr("copts", g.options(target.copts.build(), pkgRel))
   473  	}
   474  	if g.shouldSetVisibility && visibility != "" {
   475  		r.SetAttr("visibility", []string{visibility})
   476  	}
   477  	if embed != "" {
   478  		r.SetAttr("embed", []string{":" + embed})
   479  	}
   480  	r.SetPrivateAttr(config.GazelleImportsKey, target.imports.build())
   481  }
   482  
   483  func (g *generator) setImportAttrs(r *rule.Rule, importPath string) {
   484  	r.SetAttr("importpath", importPath)
   485  	goConf := getGoConfig(g.c)
   486  	if goConf.importMapPrefix != "" {
   487  		fromPrefixRel := pathtools.TrimPrefix(g.rel, goConf.importMapPrefixRel)
   488  		importMap := path.Join(goConf.importMapPrefix, fromPrefixRel)
   489  		if importMap != importPath {
   490  			r.SetAttr("importmap", importMap)
   491  		}
   492  	}
   493  }
   494  
   495  var (
   496  	// shortOptPrefixes are strings that come at the beginning of an option
   497  	// argument that includes a path, e.g., -Ifoo/bar.
   498  	shortOptPrefixes = []string{"-I", "-L", "-F"}
   499  
   500  	// longOptPrefixes are separate arguments that come before a path argument,
   501  	// e.g., -iquote foo/bar.
   502  	longOptPrefixes = []string{"-I", "-L", "-F", "-iquote", "-isystem"}
   503  )
   504  
   505  // options transforms package-relative paths in cgo options into repository-
   506  // root-relative paths that Bazel can understand. For example, if a cgo file
   507  // in //foo declares an include flag in its copts: "-Ibar", this method
   508  // will transform that flag into "-Ifoo/bar".
   509  func (g *generator) options(opts rule.PlatformStrings, pkgRel string) rule.PlatformStrings {
   510  	fixPath := func(opt string) string {
   511  		if strings.HasPrefix(opt, "/") {
   512  			return opt
   513  		}
   514  		return path.Clean(path.Join(pkgRel, opt))
   515  	}
   516  
   517  	fixGroups := func(groups []string) ([]string, error) {
   518  		fixedGroups := make([]string, len(groups))
   519  		for i, group := range groups {
   520  			opts := strings.Split(group, optSeparator)
   521  			fixedOpts := make([]string, len(opts))
   522  			isPath := false
   523  			for j, opt := range opts {
   524  				if isPath {
   525  					opt = fixPath(opt)
   526  					isPath = false
   527  					goto next
   528  				}
   529  
   530  				for _, short := range shortOptPrefixes {
   531  					if strings.HasPrefix(opt, short) && len(opt) > len(short) {
   532  						opt = short + fixPath(opt[len(short):])
   533  						goto next
   534  					}
   535  				}
   536  
   537  				for _, long := range longOptPrefixes {
   538  					if opt == long {
   539  						isPath = true
   540  						goto next
   541  					}
   542  				}
   543  
   544  			next:
   545  				fixedOpts[j] = escapeOption(opt)
   546  			}
   547  			fixedGroups[i] = strings.Join(fixedOpts, " ")
   548  		}
   549  
   550  		return fixedGroups, nil
   551  	}
   552  
   553  	opts, errs := opts.MapSlice(fixGroups)
   554  	if errs != nil {
   555  		log.Panicf("unexpected error when transforming options with pkg %q: %v", pkgRel, errs)
   556  	}
   557  	return opts
   558  }
   559  
   560  func escapeOption(opt string) string {
   561  	return strings.NewReplacer(
   562  		`\`, `\\`,
   563  		`'`, `\'`,
   564  		`"`, `\"`,
   565  		` `, `\ `,
   566  		"\t", "\\\t",
   567  		"\n", "\\\n",
   568  		"\r", "\\\r",
   569  	).Replace(opt)
   570  }