|
| 1 | +/* Copyright 2020 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 symbol generates a `symbol_library` target for every `.bzl` file in |
| 17 | +// each package. At the root of the module, a single symbol_library is |
| 18 | +// populated with deps that include all other symbol_libraries. |
| 19 | +// |
| 20 | +// The original code for this gazelle extension started from |
| 21 | +// https://github.com/bazelbuild/bazel-skylib/blob/main/gazelle/bzl/gazelle.go. |
| 22 | +package symbol |
| 23 | + |
| 24 | +import ( |
| 25 | + "flag" |
| 26 | + "fmt" |
| 27 | + "log" |
| 28 | + "os" |
| 29 | + "path/filepath" |
| 30 | + "sort" |
| 31 | + "strings" |
| 32 | + |
| 33 | + "github.com/bazelbuild/bazel-gazelle/config" |
| 34 | + "github.com/bazelbuild/bazel-gazelle/label" |
| 35 | + "github.com/bazelbuild/bazel-gazelle/language" |
| 36 | + "github.com/bazelbuild/bazel-gazelle/pathtools" |
| 37 | + "github.com/bazelbuild/bazel-gazelle/repo" |
| 38 | + "github.com/bazelbuild/bazel-gazelle/resolve" |
| 39 | + "github.com/bazelbuild/bazel-gazelle/rule" |
| 40 | + "github.com/bazelbuild/buildtools/build" |
| 41 | +) |
| 42 | + |
| 43 | +const ( |
| 44 | + languageName = "symbol" |
| 45 | + symbolLibraryKind = "symbol_library" |
| 46 | + fileType = ".bzl" |
| 47 | +) |
| 48 | + |
| 49 | +var ignoreSuffix = suffixes{ |
| 50 | + "_tests.bzl", |
| 51 | + "_test.bzl", |
| 52 | +} |
| 53 | + |
| 54 | +var kinds = map[string]rule.KindInfo{ |
| 55 | + symbolLibraryKind: { |
| 56 | + NonEmptyAttrs: map[string]bool{"srcs": true, "deps": true}, |
| 57 | + MergeableAttrs: map[string]bool{"srcs": true}, |
| 58 | + }, |
| 59 | +} |
| 60 | + |
| 61 | +type suffixes []string |
| 62 | + |
| 63 | +func (s suffixes) Matches(test string) bool { |
| 64 | + for _, v := range s { |
| 65 | + if strings.HasSuffix(test, v) { |
| 66 | + return true |
| 67 | + } |
| 68 | + } |
| 69 | + return false |
| 70 | +} |
| 71 | + |
| 72 | +type symbolLang struct { |
| 73 | + enabled bool |
| 74 | +} |
| 75 | + |
| 76 | +// NewLanguage is called by Gazelle to install this language extension in a |
| 77 | +// binary. |
| 78 | +func NewLanguage() language.Language { |
| 79 | + return &symbolLang{} |
| 80 | +} |
| 81 | + |
| 82 | +// Name returns the name of the language. This should be a prefix of the kinds |
| 83 | +// of rules generated by the language, e.g., "go" for the Go extension since it |
| 84 | +// generates "go_library" rules. |
| 85 | +func (*symbolLang) Name() string { return languageName } |
| 86 | + |
| 87 | +// The following methods are implemented to satisfy the |
| 88 | +// https://pkg.go.dev/github.com/bazelbuild/bazel-gazelle/resolve?tab=doc#Resolver |
| 89 | +// interface, but are otherwise unused. |
| 90 | +func (l *symbolLang) RegisterFlags(fs *flag.FlagSet, cmd string, c *config.Config) { |
| 91 | + fs.BoolVar(&l.enabled, "symbol_language_enabled", false, "whather this extension is turned on") |
| 92 | +} |
| 93 | +func (*symbolLang) CheckFlags(fs *flag.FlagSet, c *config.Config) error { return nil } |
| 94 | +func (*symbolLang) KnownDirectives() []string { return nil } |
| 95 | +func (*symbolLang) Configure(c *config.Config, rel string, f *rule.File) {} |
| 96 | + |
| 97 | +// Kinds returns a map of maps rule names (kinds) and information on how to |
| 98 | +// match and merge attributes that may be found in rules of those kinds. All |
| 99 | +// kinds of rules generated for this language may be found here. |
| 100 | +func (*symbolLang) Kinds() map[string]rule.KindInfo { |
| 101 | + return kinds |
| 102 | +} |
| 103 | + |
| 104 | +// Loads returns .bzl files and symbols they define. Every rule generated by |
| 105 | +// GenerateRules, now or in the past, should be loadable from one of these |
| 106 | +// files. |
| 107 | +func (*symbolLang) Loads() []rule.LoadInfo { |
| 108 | + return []rule.LoadInfo{{ |
| 109 | + Name: "@build_stack_rules_proto//rules:symbol_library.bzl", |
| 110 | + Symbols: []string{symbolLibraryKind}, |
| 111 | + }} |
| 112 | +} |
| 113 | + |
| 114 | +// Fix repairs deprecated usage of language-specific rules in f. This is called |
| 115 | +// before the file is indexed. Unless c.ShouldFix is true, fixes that delete or |
| 116 | +// rename rules should not be performed. |
| 117 | +func (*symbolLang) Fix(c *config.Config, f *rule.File) {} |
| 118 | + |
| 119 | +// Imports returns a list of ImportSpecs that can be used to import the rule r. |
| 120 | +// This is used to populate RuleIndex. |
| 121 | +// |
| 122 | +// If nil is returned, the rule will not be indexed. If any non-nil slice is |
| 123 | +// returned, including an empty slice, the rule will be indexed. |
| 124 | +func (b *symbolLang) Imports(c *config.Config, r *rule.Rule, f *rule.File) []resolve.ImportSpec { |
| 125 | + srcs := r.AttrStrings("srcs") |
| 126 | + imports := make([]resolve.ImportSpec, 0, len(srcs)) |
| 127 | + |
| 128 | + for _, src := range srcs { |
| 129 | + spec := resolve.ImportSpec{ |
| 130 | + // Lang is the language in which the import string appears (this should |
| 131 | + // match Resolver.Name). |
| 132 | + Lang: languageName, |
| 133 | + // Imp is an import string for the library. |
| 134 | + Imp: fmt.Sprintf("//%s:%s", f.Pkg, src), |
| 135 | + } |
| 136 | + |
| 137 | + imports = append(imports, spec) |
| 138 | + } |
| 139 | + |
| 140 | + return imports |
| 141 | +} |
| 142 | + |
| 143 | +// Embeds returns a list of labels of rules that the given rule embeds. If a |
| 144 | +// rule is embedded by another importable rule of the same language, only the |
| 145 | +// embedding rule will be indexed. The embedding rule will inherit the imports |
| 146 | +// of the embedded rule. Since SkyLark doesn't support embedding this should |
| 147 | +// always return nil. |
| 148 | +func (*symbolLang) Embeds(r *rule.Rule, from label.Label) []label.Label { return nil } |
| 149 | + |
| 150 | +// Resolve translates imported libraries for a given rule into Bazel |
| 151 | +// dependencies. Information about imported libraries is returned for each rule |
| 152 | +// generated by language.GenerateRules in language.GenerateResult.Imports. |
| 153 | +// Resolve generates a "deps" attribute (or the appropriate language-specific |
| 154 | +// equivalent) for each import according to language-specific rules and |
| 155 | +// heuristics. |
| 156 | +func (*symbolLang) Resolve(c *config.Config, ix *resolve.RuleIndex, rc *repo.RemoteCache, r *rule.Rule, importsRaw interface{}, from label.Label) { |
| 157 | + imports := importsRaw.([]string) |
| 158 | + |
| 159 | + r.DelAttr("deps") |
| 160 | + |
| 161 | + if len(imports) == 0 { |
| 162 | + return |
| 163 | + } |
| 164 | + |
| 165 | + deps := make([]string, 0, len(imports)) |
| 166 | + for _, imp := range imports { |
| 167 | + impLabel, err := label.Parse(imp) |
| 168 | + if err != nil { |
| 169 | + log.Printf("%s: import of %q is invalid: %v", from.String(), imp, err) |
| 170 | + continue |
| 171 | + } |
| 172 | + |
| 173 | + // the index only contains absolute labels, not relative |
| 174 | + impLabel = impLabel.Abs(from.Repo, from.Pkg) |
| 175 | + |
| 176 | + if impLabel.Repo == "bazel_tools" { |
| 177 | + // The @bazel_tools repo is tricky because it is a part of the |
| 178 | + // "shipped with bazel" core library for interacting with the |
| 179 | + // outside world. This means that it can not depend on skylib. |
| 180 | + // Fortunately there is a fairly simple workaround for this, which |
| 181 | + // is that you can add those bzl files as `deps` entries. |
| 182 | + deps = append(deps, imp) |
| 183 | + continue |
| 184 | + } |
| 185 | + |
| 186 | + if impLabel.Repo != "" || !c.IndexLibraries { |
| 187 | + // This is a dependency that is external to the current repo, or |
| 188 | + // indexing is disabled so take a guess at what the target name |
| 189 | + // should be. |
| 190 | + deps = append(deps, strings.TrimSuffix(imp, fileType)) |
| 191 | + continue |
| 192 | + } |
| 193 | + |
| 194 | + res := resolve.ImportSpec{ |
| 195 | + Lang: languageName, |
| 196 | + Imp: impLabel.String(), |
| 197 | + } |
| 198 | + matches := ix.FindRulesByImportWithConfig(c, res, languageName) |
| 199 | + if len(matches) == 0 { |
| 200 | + 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()) |
| 201 | + } |
| 202 | + |
| 203 | + for _, m := range matches { |
| 204 | + depLabel := m.Label |
| 205 | + depLabel = depLabel.Rel(from.Repo, from.Pkg) |
| 206 | + deps = append(deps, depLabel.String()) |
| 207 | + } |
| 208 | + } |
| 209 | + |
| 210 | + sort.Strings(deps) |
| 211 | + if len(deps) > 0 { |
| 212 | + r.SetAttr("deps", deps) |
| 213 | + } |
| 214 | +} |
| 215 | + |
| 216 | +// GenerateRules extracts build metadata from source files in a directory. |
| 217 | +// GenerateRules is called in each directory where an update is requested in |
| 218 | +// depth-first post-order. |
| 219 | +// |
| 220 | +// args contains the arguments for GenerateRules. This is passed as a struct to |
| 221 | +// avoid breaking implementations in the future when new fields are added. |
| 222 | +// |
| 223 | +// A GenerateResult struct is returned. Optional fields may be added to this |
| 224 | +// type in the future. |
| 225 | +// |
| 226 | +// Any non-fatal errors this function encounters should be logged using |
| 227 | +// log.Print. |
| 228 | +func (l *symbolLang) GenerateRules(args language.GenerateArgs) language.GenerateResult { |
| 229 | + if !l.enabled { |
| 230 | + return language.GenerateResult{} |
| 231 | + } |
| 232 | + |
| 233 | + var rules []*rule.Rule |
| 234 | + var imports []any |
| 235 | + for _, f := range append(args.RegularFiles, args.GenFiles...) { |
| 236 | + if !isBzlSourceFile(f) { |
| 237 | + continue |
| 238 | + } |
| 239 | + r, loads := makeSymbolLibraryRule(args, f) |
| 240 | + rules = append(rules, r) |
| 241 | + imports = append(imports, loads) |
| 242 | + } |
| 243 | + |
| 244 | + return language.GenerateResult{ |
| 245 | + Gen: rules, |
| 246 | + Imports: imports, |
| 247 | + Empty: generateEmpty(args), |
| 248 | + } |
| 249 | +} |
| 250 | + |
| 251 | +func makeSymbolLibraryRule(args language.GenerateArgs, f string) (*rule.Rule, []string) { |
| 252 | + name := strings.TrimSuffix(f, fileType) |
| 253 | + r := rule.NewRule(symbolLibraryKind, name) |
| 254 | + |
| 255 | + r.SetAttr("srcs", []string{f}) |
| 256 | + |
| 257 | + shouldSetVisibility := args.File == nil || !args.File.HasDefaultVisibility() |
| 258 | + if shouldSetVisibility { |
| 259 | + vis := checkInternalVisibility(args.Rel, "//visibility:public") |
| 260 | + r.SetAttr("visibility", []string{vis}) |
| 261 | + } |
| 262 | + |
| 263 | + fullPath := filepath.Join(args.Dir, f) |
| 264 | + loads, err := getBzlFileLoads(fullPath) |
| 265 | + |
| 266 | + if err != nil { |
| 267 | + log.Printf("%s: contains syntax errors: %v", fullPath, err) |
| 268 | + // Don't `continue` since it is reasonable to create a target even |
| 269 | + // without deps. |
| 270 | + } |
| 271 | + |
| 272 | + return r, loads |
| 273 | +} |
| 274 | + |
| 275 | +func getBzlFileLoads(path string) ([]string, error) { |
| 276 | + f, err := os.ReadFile(path) |
| 277 | + if err != nil { |
| 278 | + return nil, fmt.Errorf("os.ReadFile(%q) error: %v", path, err) |
| 279 | + } |
| 280 | + ast, err := build.ParseBuild(path, f) |
| 281 | + if err != nil { |
| 282 | + return nil, fmt.Errorf("build.Parse(%q) error: %v", f, err) |
| 283 | + } |
| 284 | + |
| 285 | + var loads []string |
| 286 | + build.WalkOnce(ast, func(expr *build.Expr) { |
| 287 | + n := *expr |
| 288 | + if l, ok := n.(*build.LoadStmt); ok { |
| 289 | + loads = append(loads, l.Module.Value) |
| 290 | + } |
| 291 | + }) |
| 292 | + sort.Strings(loads) |
| 293 | + |
| 294 | + return loads, nil |
| 295 | +} |
| 296 | + |
| 297 | +func isBzlSourceFile(f string) bool { |
| 298 | + return strings.HasSuffix(f, fileType) && !ignoreSuffix.Matches(f) |
| 299 | +} |
| 300 | + |
| 301 | +// generateEmpty generates the list of rules that don't need to exist in the |
| 302 | +// BUILD file any more. For each symbol_library rule in args.File that only has |
| 303 | +// srcs that aren't in args.RegularFiles or args.GenFiles, add a symbol_library |
| 304 | +// with no srcs or deps. That will let Gazelle delete symbol_library rules after |
| 305 | +// the corresponding .bzl files are deleted. |
| 306 | +func generateEmpty(args language.GenerateArgs) []*rule.Rule { |
| 307 | + var ret []*rule.Rule |
| 308 | + if args.File == nil { |
| 309 | + return ret |
| 310 | + } |
| 311 | + for _, r := range args.File.Rules { |
| 312 | + if r.Kind() != symbolLibraryKind { |
| 313 | + continue |
| 314 | + } |
| 315 | + name := r.AttrString("name") |
| 316 | + |
| 317 | + exists := make(map[string]bool) |
| 318 | + for _, f := range args.RegularFiles { |
| 319 | + exists[f] = true |
| 320 | + } |
| 321 | + for _, f := range args.GenFiles { |
| 322 | + exists[f] = true |
| 323 | + } |
| 324 | + for _, r := range args.File.Rules { |
| 325 | + srcsExist := false |
| 326 | + for _, f := range r.AttrStrings("srcs") { |
| 327 | + if exists[f] { |
| 328 | + srcsExist = true |
| 329 | + break |
| 330 | + } |
| 331 | + } |
| 332 | + if !srcsExist { |
| 333 | + ret = append(ret, rule.NewRule(symbolLibraryKind, name)) |
| 334 | + } |
| 335 | + } |
| 336 | + } |
| 337 | + return ret |
| 338 | +} |
| 339 | + |
| 340 | +// checkInternalVisibility overrides the given visibility if the package is |
| 341 | +// internal. |
| 342 | +func checkInternalVisibility(rel, visibility string) string { |
| 343 | + if i := pathtools.Index(rel, "internal"); i > 0 { |
| 344 | + visibility = fmt.Sprintf("//%s:__subpackages__", rel[:i-1]) |
| 345 | + } else if i := pathtools.Index(rel, "private"); i > 0 { |
| 346 | + visibility = fmt.Sprintf("//%s:__subpackages__", rel[:i-1]) |
| 347 | + } else if pathtools.HasPrefix(rel, "internal") || pathtools.HasPrefix(rel, "private") { |
| 348 | + visibility = "//:__subpackages__" |
| 349 | + } |
| 350 | + return visibility |
| 351 | +} |
0 commit comments