Skip to content

Commit 8e0b4fc

Browse files
committed
Initial symbol extension
1 parent 63a0ce3 commit 8e0b4fc

28 files changed

Lines changed: 1461 additions & 2 deletions

File tree

WORKSPACE

Whitespace-only changes.

cmd/gazelle/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ go_library(
2929
"//cmd/gazelle/internal/wspace",
3030
"//language/proto_go_modules",
3131
"//language/protobuf",
32+
"//language/symbol",
3233
"@bazel_gazelle//config:go_default_library",
3334
"@bazel_gazelle//flag:go_default_library",
3435
"@bazel_gazelle//label:go_default_library",

cmd/gazelle/langs.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ import (
2121
"github.com/bazelbuild/bazel-gazelle/language/proto"
2222
"github.com/stackb/rules_proto/language/proto_go_modules"
2323
"github.com/stackb/rules_proto/language/protobuf"
24+
"github.com/stackb/rules_proto/language/symbol"
2425
)
2526

2627
var languages = []language.Language{
2728
proto.NewLanguage(),
2829
protobuf.NewLanguage(),
2930
golang.NewLanguage(),
3031
proto_go_modules.NewLanguage(),
32+
symbol.NewLanguage(),
3133
}

language/symbol/BUILD.bazel

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
load("@io_bazel_rules_go//go:def.bzl", "go_library")
2+
3+
go_library(
4+
name = "symbol",
5+
srcs = ["symbol.go"],
6+
importpath = "github.com/stackb/rules_proto/language/symbol",
7+
visibility = ["//visibility:public"],
8+
deps = [
9+
"@bazel_gazelle//config:go_default_library",
10+
"@bazel_gazelle//label:go_default_library",
11+
"@bazel_gazelle//language:go_default_library",
12+
"@bazel_gazelle//pathtools:go_default_library",
13+
"@bazel_gazelle//repo:go_default_library",
14+
"@bazel_gazelle//resolve:go_default_library",
15+
"@bazel_gazelle//rule:go_default_library",
16+
"@com_github_bazelbuild_buildtools//build:go_default_library",
17+
],
18+
)

language/symbol/symbol.go

Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
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

Comments
 (0)