| /* Copyright 2020 The Bazel Authors. All rights reserved. |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| // Package bzl generates a `bzl_library` target for every `.bzl` file in |
| // each package. |
| // |
| // The `bzl_library` rule is provided by |
| // https://github.com/bazelbuild/bazel-skylib. |
| // |
| // This extension is experimental and subject to change. It is not included |
| // in the default Gazelle binary. |
| package bzl |
| |
| import ( |
| "flag" |
| "fmt" |
| "io/ioutil" |
| "log" |
| "path/filepath" |
| "sort" |
| "strings" |
| |
| "github.com/bazelbuild/bazel-gazelle/config" |
| "github.com/bazelbuild/bazel-gazelle/label" |
| "github.com/bazelbuild/bazel-gazelle/language" |
| "github.com/bazelbuild/bazel-gazelle/pathtools" |
| "github.com/bazelbuild/bazel-gazelle/repo" |
| "github.com/bazelbuild/bazel-gazelle/resolve" |
| "github.com/bazelbuild/bazel-gazelle/rule" |
| |
| "github.com/bazelbuild/buildtools/build" |
| ) |
| |
| const languageName = "starlark" |
| const fileType = ".bzl" |
| |
| var ignoreSuffix = suffixes{ |
| "_tests.bzl", |
| "_test.bzl", |
| } |
| |
| type suffixes []string |
| |
| func (s suffixes) Matches(test string) bool { |
| for _, v := range s { |
| if strings.HasSuffix(test, v) { |
| return true |
| } |
| } |
| return false |
| } |
| |
| type bzlLibraryLang struct{} |
| |
| // NewLanguage is called by Gazelle to install this language extension in a binary. |
| func NewLanguage() language.Language { |
| return &bzlLibraryLang{} |
| } |
| |
| // Name returns the name of the language. This should be a prefix of the |
| // kinds of rules generated by the language, e.g., "go" for the Go extension |
| // since it generates "go_library" rules. |
| func (*bzlLibraryLang) Name() string { return languageName } |
| |
| // The following methods are implemented to satisfy the |
| // https://pkg.go.dev/github.com/bazelbuild/bazel-gazelle/resolve?tab=doc#Resolver |
| // interface, but are otherwise unused. |
| func (*bzlLibraryLang) RegisterFlags(fs *flag.FlagSet, cmd string, c *config.Config) {} |
| func (*bzlLibraryLang) CheckFlags(fs *flag.FlagSet, c *config.Config) error { return nil } |
| func (*bzlLibraryLang) KnownDirectives() []string { return nil } |
| func (*bzlLibraryLang) Configure(c *config.Config, rel string, f *rule.File) {} |
| |
| // Kinds returns a map of maps rule names (kinds) and information on how to |
| // match and merge attributes that may be found in rules of those kinds. All |
| // kinds of rules generated for this language may be found here. |
| func (*bzlLibraryLang) Kinds() map[string]rule.KindInfo { |
| return kinds |
| } |
| |
| // Loads returns .bzl files and symbols they define. Every rule generated by |
| // GenerateRules, now or in the past, should be loadable from one of these |
| // files. |
| func (*bzlLibraryLang) Loads() []rule.LoadInfo { |
| return []rule.LoadInfo{{ |
| Name: "@bazel_skylib//:bzl_library.bzl", |
| Symbols: []string{"bzl_library"}, |
| }} |
| } |
| |
| // Fix repairs deprecated usage of language-specific rules in f. This is |
| // called before the file is indexed. Unless c.ShouldFix is true, fixes |
| // that delete or rename rules should not be performed. |
| func (*bzlLibraryLang) Fix(c *config.Config, f *rule.File) {} |
| |
| // Imports returns a list of ImportSpecs that can be used to import the rule |
| // r. This is used to populate RuleIndex. |
| // |
| // If nil is returned, the rule will not be indexed. If any non-nil slice is |
| // returned, including an empty slice, the rule will be indexed. |
| func (b *bzlLibraryLang) Imports(c *config.Config, r *rule.Rule, f *rule.File) []resolve.ImportSpec { |
| srcs := r.AttrStrings("srcs") |
| imports := make([]resolve.ImportSpec, len(srcs)) |
| |
| for _, src := range srcs { |
| spec := resolve.ImportSpec{ |
| // Lang is the language in which the import string appears (this should |
| // match Resolver.Name). |
| Lang: languageName, |
| // Imp is an import string for the library. |
| Imp: fmt.Sprintf("//%s:%s", f.Pkg, src), |
| } |
| |
| imports = append(imports, spec) |
| } |
| |
| return imports |
| } |
| |
| // Embeds returns a list of labels of rules that the given rule embeds. If |
| // a rule is embedded by another importable rule of the same language, only |
| // the embedding rule will be indexed. The embedding rule will inherit |
| // the imports of the embedded rule. |
| // Since SkyLark doesn't support embedding this should always return nil. |
| func (*bzlLibraryLang) Embeds(r *rule.Rule, from label.Label) []label.Label { return nil } |
| |
| // Resolve translates imported libraries for a given rule into Bazel |
| // dependencies. Information about imported libraries is returned for each |
| // rule generated by language.GenerateRules in |
| // language.GenerateResult.Imports. Resolve generates a "deps" attribute (or |
| // the appropriate language-specific equivalent) for each import according to |
| // language-specific rules and heuristics. |
| func (*bzlLibraryLang) Resolve(c *config.Config, ix *resolve.RuleIndex, rc *repo.RemoteCache, r *rule.Rule, importsRaw interface{}, from label.Label) { |
| imports := importsRaw.([]string) |
| |
| r.DelAttr("deps") |
| |
| if len(imports) == 0 { |
| return |
| } |
| |
| deps := make([]string, 0, len(imports)) |
| for _, imp := range imports { |
| impLabel, err := label.Parse(imp) |
| if err != nil { |
| log.Printf("%s: import of %q is invalid: %v", from.String(), imp, err) |
| continue |
| } |
| |
| // the index only contains absolute labels, not relative |
| impLabel = impLabel.Abs(from.Repo, from.Pkg) |
| |
| if impLabel.Repo == "bazel_tools" { |
| // The @bazel_tools repo is tricky because it is a part of the "shipped |
| // with bazel" core library for interacting with the outside world. |
| // This means that it can not depend on skylib. Fortunately there is a |
| // fairly simple workaround for this, which is that you can add those |
| // bzl files as `deps` entries. |
| deps = append(deps, imp) |
| continue |
| } |
| |
| if impLabel.Repo != "" || !c.IndexLibraries { |
| // This is a dependency that is external to the current repo, or indexing |
| // is disabled so take a guess at what hte target name should be. |
| deps = append(deps, strings.TrimSuffix(imp, fileType)) |
| continue |
| } |
| |
| res := resolve.ImportSpec{ |
| Lang: languageName, |
| Imp: impLabel.String(), |
| } |
| matches := ix.FindRulesByImport(res, languageName) |
| |
| if len(matches) == 0 { |
| log.Printf("%s: %q (%s) was not found in dependency index. Skipping. This may result in an incomplete deps section and require manual BUILD file intervention.\n", from.String(), imp, impLabel.String()) |
| } |
| |
| for _, m := range matches { |
| depLabel := m.Label |
| depLabel = depLabel.Rel(from.Repo, from.Pkg) |
| deps = append(deps, depLabel.String()) |
| } |
| } |
| |
| sort.Strings(deps) |
| if len(deps) > 0 { |
| r.SetAttr("deps", deps) |
| } |
| } |
| |
| var kinds = map[string]rule.KindInfo{ |
| "bzl_library": { |
| NonEmptyAttrs: map[string]bool{"srcs": true, "deps": true}, |
| MergeableAttrs: map[string]bool{"srcs": true}, |
| }, |
| } |
| |
| // GenerateRules extracts build metadata from source files in a directory. |
| // GenerateRules is called in each directory where an update is requested |
| // in depth-first post-order. |
| // |
| // args contains the arguments for GenerateRules. This is passed as a |
| // struct to avoid breaking implementations in the future when new |
| // fields are added. |
| // |
| // A GenerateResult struct is returned. Optional fields may be added to this |
| // type in the future. |
| // |
| // Any non-fatal errors this function encounters should be logged using |
| // log.Print. |
| func (*bzlLibraryLang) GenerateRules(args language.GenerateArgs) language.GenerateResult { |
| var rules []*rule.Rule |
| var imports []interface{} |
| for _, f := range append(args.RegularFiles, args.GenFiles...) { |
| if !isBzlSourceFile(f) { |
| continue |
| } |
| name := strings.TrimSuffix(f, fileType) |
| r := rule.NewRule("bzl_library", name) |
| |
| r.SetAttr("srcs", []string{f}) |
| |
| shouldSetVisibility := args.File == nil || !args.File.HasDefaultVisibility() |
| if shouldSetVisibility { |
| vis := checkInternalVisibility(args.Rel, "//visibility:public") |
| r.SetAttr("visibility", []string{vis}) |
| } |
| |
| fullPath := filepath.Join(args.Dir, f) |
| loads, err := getBzlFileLoads(fullPath) |
| if err != nil { |
| log.Printf("%s: contains syntax errors: %v", fullPath, err) |
| // Don't `continue` since it is reasonable to create a target even |
| // without deps. |
| } |
| |
| rules = append(rules, r) |
| imports = append(imports, loads) |
| } |
| |
| return language.GenerateResult{ |
| Gen: rules, |
| Imports: imports, |
| Empty: generateEmpty(args), |
| } |
| } |
| |
| func getBzlFileLoads(path string) ([]string, error) { |
| f, err := ioutil.ReadFile(path) |
| if err != nil { |
| return nil, fmt.Errorf("ioutil.ReadFile(%q) error: %v", path, err) |
| } |
| ast, err := build.ParseBuild(path, f) |
| if err != nil { |
| return nil, fmt.Errorf("build.Parse(%q) error: %v", f, err) |
| } |
| |
| var loads []string |
| build.WalkOnce(ast, func(expr *build.Expr) { |
| n := *expr |
| if l, ok := n.(*build.LoadStmt); ok { |
| loads = append(loads, l.Module.Value) |
| } |
| }) |
| sort.Strings(loads) |
| |
| return loads, nil |
| } |
| |
| func isBzlSourceFile(f string) bool { |
| return strings.HasSuffix(f, fileType) && !ignoreSuffix.Matches(f) |
| } |
| |
| // generateEmpty generates the list of rules that don't need to exist in the |
| // BUILD file any more. |
| // For each bzl_library rule in args.File that only has srcs that aren't in |
| // args.RegularFiles or args.GenFiles, add a bzl_library with no srcs or deps. |
| // That will let Gazelle delete bzl_library rules after the corresponding .bzl |
| // files are deleted. |
| func generateEmpty(args language.GenerateArgs) []*rule.Rule { |
| var ret []*rule.Rule |
| if args.File == nil { |
| return ret |
| } |
| for _, r := range args.File.Rules { |
| if r.Kind() != "bzl_library" { |
| continue |
| } |
| name := r.AttrString("name") |
| |
| exists := make(map[string]bool) |
| for _, f := range args.RegularFiles { |
| exists[f] = true |
| } |
| for _, f := range args.GenFiles { |
| exists[f] = true |
| } |
| for _, r := range args.File.Rules { |
| srcsExist := false |
| for _, f := range r.AttrStrings("srcs") { |
| if exists[f] { |
| srcsExist = true |
| break |
| } |
| } |
| if !srcsExist { |
| ret = append(ret, rule.NewRule("bzl_library", name)) |
| } |
| } |
| } |
| return ret |
| } |
| |
| type srcsList []string |
| |
| func (s srcsList) Contains(m string) bool { |
| for _, e := range s { |
| if e == m { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // checkInternalVisibility overrides the given visibility if the package is |
| // internal. |
| func checkInternalVisibility(rel, visibility string) string { |
| if i := pathtools.Index(rel, "internal"); i > 0 { |
| visibility = fmt.Sprintf("//%s:__subpackages__", rel[:i-1]) |
| } else if i := pathtools.Index(rel, "private"); i > 0 { |
| visibility = fmt.Sprintf("//%s:__subpackages__", rel[:i-1]) |
| } else if pathtools.HasPrefix(rel, "internal") || pathtools.HasPrefix(rel, "private") { |
| visibility = "//:__subpackages__" |
| } |
| return visibility |
| } |