1/* Copyright 2020 The Bazel Authors. All rights reserved. 2 3Licensed under the Apache License, Version 2.0 (the "License"); 4you may not use this file except in compliance with the License. 5You may obtain a copy of the License at 6 7 http://www.apache.org/licenses/LICENSE-2.0 8 9Unless required by applicable law or agreed to in writing, software 10distributed under the License is distributed on an "AS IS" BASIS, 11WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12See the License for the specific language governing permissions and 13limitations under the License. 14*/ 15 16// Package bzl generates a `bzl_library` target for every `.bzl` file in 17// each package. 18// 19// The `bzl_library` rule is provided by 20// https://github.com/bazelbuild/bazel-skylib. 21// 22// This extension is experimental and subject to change. It is not included 23// in the default Gazelle binary. 24package bzl 25 26import ( 27 "flag" 28 "fmt" 29 "io/ioutil" 30 "log" 31 "path/filepath" 32 "sort" 33 "strings" 34 35 "github.com/bazelbuild/bazel-gazelle/config" 36 "github.com/bazelbuild/bazel-gazelle/label" 37 "github.com/bazelbuild/bazel-gazelle/language" 38 "github.com/bazelbuild/bazel-gazelle/pathtools" 39 "github.com/bazelbuild/bazel-gazelle/repo" 40 "github.com/bazelbuild/bazel-gazelle/resolve" 41 "github.com/bazelbuild/bazel-gazelle/rule" 42 43 "github.com/bazelbuild/buildtools/build" 44) 45 46const languageName = "starlark" 47const fileType = ".bzl" 48 49var ignoreSuffix = suffixes{ 50 "_tests.bzl", 51 "_test.bzl", 52} 53 54type suffixes []string 55 56func (s suffixes) Matches(test string) bool { 57 for _, v := range s { 58 if strings.HasSuffix(test, v) { 59 return true 60 } 61 } 62 return false 63} 64 65type bzlLibraryLang struct{} 66 67// NewLanguage is called by Gazelle to install this language extension in a binary. 68func NewLanguage() language.Language { 69 return &bzlLibraryLang{} 70} 71 72// Name returns the name of the language. This should be a prefix of the 73// kinds of rules generated by the language, e.g., "go" for the Go extension 74// since it generates "go_library" rules. 75func (*bzlLibraryLang) Name() string { return languageName } 76 77// The following methods are implemented to satisfy the 78// https://pkg.go.dev/github.com/bazelbuild/bazel-gazelle/resolve?tab=doc#Resolver 79// interface, but are otherwise unused. 80func (*bzlLibraryLang) RegisterFlags(fs *flag.FlagSet, cmd string, c *config.Config) {} 81func (*bzlLibraryLang) CheckFlags(fs *flag.FlagSet, c *config.Config) error { return nil } 82func (*bzlLibraryLang) KnownDirectives() []string { return nil } 83func (*bzlLibraryLang) Configure(c *config.Config, rel string, f *rule.File) {} 84 85// Kinds returns a map of maps rule names (kinds) and information on how to 86// match and merge attributes that may be found in rules of those kinds. All 87// kinds of rules generated for this language may be found here. 88func (*bzlLibraryLang) Kinds() map[string]rule.KindInfo { 89 return kinds 90} 91 92// Loads returns .bzl files and symbols they define. Every rule generated by 93// GenerateRules, now or in the past, should be loadable from one of these 94// files. 95func (*bzlLibraryLang) Loads() []rule.LoadInfo { 96 return []rule.LoadInfo{{ 97 Name: "@bazel_skylib//:bzl_library.bzl", 98 Symbols: []string{"bzl_library"}, 99 }} 100} 101 102// Fix repairs deprecated usage of language-specific rules in f. This is 103// called before the file is indexed. Unless c.ShouldFix is true, fixes 104// that delete or rename rules should not be performed. 105func (*bzlLibraryLang) Fix(c *config.Config, f *rule.File) {} 106 107// Imports returns a list of ImportSpecs that can be used to import the rule 108// r. This is used to populate RuleIndex. 109// 110// If nil is returned, the rule will not be indexed. If any non-nil slice is 111// returned, including an empty slice, the rule will be indexed. 112func (b *bzlLibraryLang) Imports(c *config.Config, r *rule.Rule, f *rule.File) []resolve.ImportSpec { 113 srcs := r.AttrStrings("srcs") 114 imports := make([]resolve.ImportSpec, len(srcs)) 115 116 for _, src := range srcs { 117 spec := resolve.ImportSpec{ 118 // Lang is the language in which the import string appears (this should 119 // match Resolver.Name). 120 Lang: languageName, 121 // Imp is an import string for the library. 122 Imp: fmt.Sprintf("//%s:%s", f.Pkg, src), 123 } 124 125 imports = append(imports, spec) 126 } 127 128 return imports 129} 130 131// Embeds returns a list of labels of rules that the given rule embeds. If 132// a rule is embedded by another importable rule of the same language, only 133// the embedding rule will be indexed. The embedding rule will inherit 134// the imports of the embedded rule. 135// Since SkyLark doesn't support embedding this should always return nil. 136func (*bzlLibraryLang) Embeds(r *rule.Rule, from label.Label) []label.Label { return nil } 137 138// Resolve translates imported libraries for a given rule into Bazel 139// dependencies. Information about imported libraries is returned for each 140// rule generated by language.GenerateRules in 141// language.GenerateResult.Imports. Resolve generates a "deps" attribute (or 142// the appropriate language-specific equivalent) for each import according to 143// language-specific rules and heuristics. 144func (*bzlLibraryLang) Resolve(c *config.Config, ix *resolve.RuleIndex, rc *repo.RemoteCache, r *rule.Rule, importsRaw interface{}, from label.Label) { 145 imports := importsRaw.([]string) 146 147 r.DelAttr("deps") 148 149 if len(imports) == 0 { 150 return 151 } 152 153 deps := make([]string, 0, len(imports)) 154 for _, imp := range imports { 155 if strings.HasPrefix(imp, "@") || !c.IndexLibraries { 156 // This is a dependency that is external to the current repo, or indexing 157 // is disabled so take a guess at what hte target name should be. 158 deps = append(deps, strings.TrimSuffix(imp, fileType)) 159 } else { 160 res := resolve.ImportSpec{ 161 Lang: languageName, 162 Imp: imp, 163 } 164 matches := ix.FindRulesByImport(res, languageName) 165 166 if len(matches) == 0 { 167 log.Printf("%s: %q 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) 168 } 169 170 for _, m := range matches { 171 deps = append(deps, m.Label.String()) 172 } 173 } 174 } 175 176 sort.Strings(deps) 177 if len(deps) > 0 { 178 r.SetAttr("deps", deps) 179 } 180} 181 182var kinds = map[string]rule.KindInfo{ 183 "bzl_library": { 184 NonEmptyAttrs: map[string]bool{"srcs": true, "deps": true}, 185 MergeableAttrs: map[string]bool{"srcs": true}, 186 }, 187} 188 189// GenerateRules extracts build metadata from source files in a directory. 190// GenerateRules is called in each directory where an update is requested 191// in depth-first post-order. 192// 193// args contains the arguments for GenerateRules. This is passed as a 194// struct to avoid breaking implementations in the future when new 195// fields are added. 196// 197// A GenerateResult struct is returned. Optional fields may be added to this 198// type in the future. 199// 200// Any non-fatal errors this function encounters should be logged using 201// log.Print. 202func (*bzlLibraryLang) GenerateRules(args language.GenerateArgs) language.GenerateResult { 203 var rules []*rule.Rule 204 var imports []interface{} 205 for _, f := range append(args.RegularFiles, args.GenFiles...) { 206 if !isBzlSourceFile(f) { 207 continue 208 } 209 name := strings.TrimSuffix(f, fileType) 210 r := rule.NewRule("bzl_library", name) 211 212 r.SetAttr("srcs", []string{f}) 213 214 if args.File == nil || !args.File.HasDefaultVisibility() { 215 inPrivateDir := pathtools.Index(args.Rel, "private") >= 0 216 if !inPrivateDir { 217 r.SetAttr("visibility", []string{"//visibility:public"}) 218 } 219 } 220 221 fullPath := filepath.Join(args.Dir, f) 222 loads, err := getBzlFileLoads(fullPath) 223 if err != nil { 224 log.Printf("%s: contains syntax errors: %v", fullPath, err) 225 // Don't `continue` since it is reasonable to create a target even 226 // without deps. 227 } 228 229 rules = append(rules, r) 230 imports = append(imports, loads) 231 } 232 233 return language.GenerateResult{ 234 Gen: rules, 235 Imports: imports, 236 Empty: generateEmpty(args), 237 } 238} 239 240func getBzlFileLoads(path string) ([]string, error) { 241 f, err := ioutil.ReadFile(path) 242 if err != nil { 243 return nil, fmt.Errorf("ioutil.ReadFile(%q) error: %v", path, err) 244 } 245 ast, err := build.ParseBuild(path, f) 246 if err != nil { 247 return nil, fmt.Errorf("build.Parse(%q) error: %v", f, err) 248 } 249 250 var loads []string 251 build.WalkOnce(ast, func(expr *build.Expr) { 252 n := *expr 253 if l, ok := n.(*build.LoadStmt); ok { 254 loads = append(loads, l.Module.Value) 255 } 256 }) 257 sort.Strings(loads) 258 259 return loads, nil 260} 261 262func isBzlSourceFile(f string) bool { 263 return strings.HasSuffix(f, fileType) && !ignoreSuffix.Matches(f) 264} 265 266// generateEmpty generates the list of rules that don't need to exist in the 267// BUILD file any more. 268// For each bzl_library rule in args.File that only has srcs that aren't in 269// args.RegularFiles or args.GenFiles, add a bzl_library with no srcs or deps. 270// That will let Gazelle delete bzl_library rules after the corresponding .bzl 271// files are deleted. 272func generateEmpty(args language.GenerateArgs) []*rule.Rule { 273 var ret []*rule.Rule 274 if args.File == nil { 275 return ret 276 } 277 for _, r := range args.File.Rules { 278 if r.Kind() != "bzl_library" { 279 continue 280 } 281 name := r.AttrString("name") 282 283 exists := make(map[string]bool) 284 for _, f := range args.RegularFiles { 285 exists[f] = true 286 } 287 for _, f := range args.GenFiles { 288 exists[f] = true 289 } 290 for _, r := range args.File.Rules { 291 srcsExist := false 292 for _, f := range r.AttrStrings("srcs") { 293 if exists[f] { 294 srcsExist = true 295 break 296 } 297 } 298 if !srcsExist { 299 ret = append(ret, rule.NewRule("bzl_library", name)) 300 } 301 } 302 } 303 return ret 304} 305 306type srcsList []string 307 308func (s srcsList) Contains(m string) bool { 309 for _, e := range s { 310 if e == m { 311 return true 312 } 313 } 314 return false 315} 316