xref: /aosp_15_r20/external/bazel-skylib/gazelle/bzl/gazelle.go (revision bcb5dc7965af6ee42bf2f21341a2ec00233a8c8a)
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