• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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