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, 0, 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 impLabel, err := label.Parse(imp) 156 if err != nil { 157 log.Printf("%s: import of %q is invalid: %v", from.String(), imp, err) 158 continue 159 } 160 161 // the index only contains absolute labels, not relative 162 impLabel = impLabel.Abs(from.Repo, from.Pkg) 163 164 if impLabel.Repo == "bazel_tools" { 165 // The @bazel_tools repo is tricky because it is a part of the "shipped 166 // with bazel" core library for interacting with the outside world. 167 // This means that it can not depend on skylib. Fortunately there is a 168 // fairly simple workaround for this, which is that you can add those 169 // bzl files as `deps` entries. 170 deps = append(deps, imp) 171 continue 172 } 173 174 if impLabel.Repo != "" || !c.IndexLibraries { 175 // This is a dependency that is external to the current repo, or indexing 176 // is disabled so take a guess at what the target name should be. 177 deps = append(deps, strings.TrimSuffix(imp, fileType)) 178 continue 179 } 180 181 res := resolve.ImportSpec{ 182 Lang: languageName, 183 Imp: impLabel.String(), 184 } 185 matches := ix.FindRulesByImport(res, languageName) 186 187 if len(matches) == 0 { 188 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()) 189 } 190 191 for _, m := range matches { 192 depLabel := m.Label 193 depLabel = depLabel.Rel(from.Repo, from.Pkg) 194 deps = append(deps, depLabel.String()) 195 } 196 } 197 198 sort.Strings(deps) 199 if len(deps) > 0 { 200 r.SetAttr("deps", deps) 201 } 202} 203 204var kinds = map[string]rule.KindInfo{ 205 "bzl_library": { 206 NonEmptyAttrs: map[string]bool{"srcs": true, "deps": true}, 207 MergeableAttrs: map[string]bool{"srcs": true}, 208 }, 209} 210 211// GenerateRules extracts build metadata from source files in a directory. 212// GenerateRules is called in each directory where an update is requested 213// in depth-first post-order. 214// 215// args contains the arguments for GenerateRules. This is passed as a 216// struct to avoid breaking implementations in the future when new 217// fields are added. 218// 219// A GenerateResult struct is returned. Optional fields may be added to this 220// type in the future. 221// 222// Any non-fatal errors this function encounters should be logged using 223// log.Print. 224func (*bzlLibraryLang) GenerateRules(args language.GenerateArgs) language.GenerateResult { 225 var rules []*rule.Rule 226 var imports []interface{} 227 for _, f := range append(args.RegularFiles, args.GenFiles...) { 228 if !isBzlSourceFile(f) { 229 continue 230 } 231 name := strings.TrimSuffix(f, fileType) 232 r := rule.NewRule("bzl_library", name) 233 234 r.SetAttr("srcs", []string{f}) 235 236 shouldSetVisibility := args.File == nil || !args.File.HasDefaultVisibility() 237 if shouldSetVisibility { 238 vis := checkInternalVisibility(args.Rel, "//visibility:public") 239 r.SetAttr("visibility", []string{vis}) 240 } 241 242 fullPath := filepath.Join(args.Dir, f) 243 loads, err := getBzlFileLoads(fullPath) 244 if err != nil { 245 log.Printf("%s: contains syntax errors: %v", fullPath, err) 246 // Don't `continue` since it is reasonable to create a target even 247 // without deps. 248 } 249 250 rules = append(rules, r) 251 imports = append(imports, loads) 252 } 253 254 return language.GenerateResult{ 255 Gen: rules, 256 Imports: imports, 257 Empty: generateEmpty(args), 258 } 259} 260 261func getBzlFileLoads(path string) ([]string, error) { 262 f, err := ioutil.ReadFile(path) 263 if err != nil { 264 return nil, fmt.Errorf("ioutil.ReadFile(%q) error: %v", path, err) 265 } 266 ast, err := build.ParseBuild(path, f) 267 if err != nil { 268 return nil, fmt.Errorf("build.Parse(%q) error: %v", f, err) 269 } 270 271 var loads []string 272 build.WalkOnce(ast, func(expr *build.Expr) { 273 n := *expr 274 if l, ok := n.(*build.LoadStmt); ok { 275 loads = append(loads, l.Module.Value) 276 } 277 }) 278 sort.Strings(loads) 279 280 return loads, nil 281} 282 283func isBzlSourceFile(f string) bool { 284 return strings.HasSuffix(f, fileType) && !ignoreSuffix.Matches(f) 285} 286 287// generateEmpty generates the list of rules that don't need to exist in the 288// BUILD file any more. 289// For each bzl_library rule in args.File that only has srcs that aren't in 290// args.RegularFiles or args.GenFiles, add a bzl_library with no srcs or deps. 291// That will let Gazelle delete bzl_library rules after the corresponding .bzl 292// files are deleted. 293func generateEmpty(args language.GenerateArgs) []*rule.Rule { 294 var ret []*rule.Rule 295 if args.File == nil { 296 return ret 297 } 298 for _, r := range args.File.Rules { 299 if r.Kind() != "bzl_library" { 300 continue 301 } 302 name := r.AttrString("name") 303 304 exists := make(map[string]bool) 305 for _, f := range args.RegularFiles { 306 exists[f] = true 307 } 308 for _, f := range args.GenFiles { 309 exists[f] = true 310 } 311 for _, r := range args.File.Rules { 312 srcsExist := false 313 for _, f := range r.AttrStrings("srcs") { 314 if exists[f] { 315 srcsExist = true 316 break 317 } 318 } 319 if !srcsExist { 320 ret = append(ret, rule.NewRule("bzl_library", name)) 321 } 322 } 323 } 324 return ret 325} 326 327type srcsList []string 328 329func (s srcsList) Contains(m string) bool { 330 for _, e := range s { 331 if e == m { 332 return true 333 } 334 } 335 return false 336} 337 338// checkInternalVisibility overrides the given visibility if the package is 339// internal. 340func checkInternalVisibility(rel, visibility string) string { 341 if i := pathtools.Index(rel, "internal"); i > 0 { 342 visibility = fmt.Sprintf("//%s:__subpackages__", rel[:i-1]) 343 } else if i := pathtools.Index(rel, "private"); i > 0 { 344 visibility = fmt.Sprintf("//%s:__subpackages__", rel[:i-1]) 345 } else if pathtools.HasPrefix(rel, "internal") || pathtools.HasPrefix(rel, "private") { 346 visibility = "//:__subpackages__" 347 } 348 return visibility 349} 350