Skip to content

Commit

Permalink
Add visibility extension to support recursive default_visibility conf…
Browse files Browse the repository at this point in the history
…iguration (bazelbuild#783)

Under a new /bazel/ subdirectory, this language-agnostic extension allows a directive to control the default_visibility templated out to all BUILD.bazel files in or under the directory. This feature is intended to allow large codebases to define hierarchical visibility defaults.
  • Loading branch information
dnathe4th committed Oct 13, 2022
1 parent fe1935f commit a56b4c8
Show file tree
Hide file tree
Showing 8 changed files with 291 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -6,3 +6,4 @@
/tests/bcr/bazel-bin
/tests/bcr/bazel-out
/tests/bcr/bazel-testlogs
.DS_STORE
1 change: 1 addition & 0 deletions language/BUILD.bazel
Expand Up @@ -26,6 +26,7 @@ filegroup(
"base.go",
"lang.go",
"update.go",
"//language/bazel:all_files",
"//language/go:all_files",
"//language/proto:all_files",
],
Expand Down
9 changes: 9 additions & 0 deletions language/bazel/BUILD.bazel
@@ -0,0 +1,9 @@
filegroup(
name = "all_files",
testonly = True,
srcs = [
"BUILD.bazel",
"//language/bazel/visibility:all_files",
],
visibility = ["//visibility:public"],
)
52 changes: 52 additions & 0 deletions language/bazel/visibility/BUILD.bazel
@@ -0,0 +1,52 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "visibility",
srcs = [
"config.go",
"lang.go",
"resolve.go",
],
importpath = "github.com/bazelbuild/bazel-gazelle/language/bazel/visibility",
visibility = ["//visibility:public"],
deps = [
"//config",
"//label",
"//language",
"//repo",
"//resolve",
"//rule",
],
)

alias(
name = "go_default_library",
actual = ":visibility",
visibility = ["//visibility:public"],
)

go_test(
name = "visibility_test",
srcs = ["lang_test.go"],
deps = [
":visibility",
"//config",
"//label",
"//language",
"//rule",
"@com_github_stretchr_testify//require:go_default_library",
],
)

filegroup(
name = "all_files",
testonly = True,
srcs = [
"BUILD.bazel",
"config.go",
"lang.go",
"lang_test.go",
"resolve.go",
],
visibility = ["//visibility:public"],
)
64 changes: 64 additions & 0 deletions language/bazel/visibility/config.go
@@ -0,0 +1,64 @@
package visibility

import (
"flag"
"strings"

"github.com/bazelbuild/bazel-gazelle/config"
"github.com/bazelbuild/bazel-gazelle/rule"
)

const (
_directiveName = "default_visibility"
)

type visConfig struct {
visibilityTargets []string
}

// getVisConfig directly returns the internal configuration struct rather
// than a pointer because we explicitly want pass-by-value symantics so
// configurations down a directory tree don't accidentially update upstream.
func getVisConfig(c *config.Config) visConfig {
cfg := c.Exts[_extName]
if cfg == nil {
return visConfig{}
}
return cfg.(visConfig)
}

// RegisterFlags noops because we only parameterize behavior with a directive.
func (*visibilityExtension) RegisterFlags(fs *flag.FlagSet, cmd string, c *config.Config) {}

// CheckFlags noops because no flags are referenced.
func (*visibilityExtension) CheckFlags(fs *flag.FlagSet, c *config.Config) error {
return nil
}

// KnownDirectives returns the only directive this extension operates on.
func (*visibilityExtension) KnownDirectives() []string {
return []string{_directiveName}
}

// Configure identifies the visibility targets from the directive value, if it exists.
//
// To set multiple visibility targets, either multiple directives can be used, or a
// list can be provided with comma-separated values.
func (*visibilityExtension) Configure(c *config.Config, _ string, f *rule.File) {
cfg := getVisConfig(c)
if f == nil {
return
}

for _, d := range f.Directives {
switch d.Key {
case _directiveName:
for _, target := range strings.Split(d.Value, ",") {
cfg.visibilityTargets = append(cfg.visibilityTargets, target)
}
}
}
c.Exts[_extName] = cfg
}

// /Configurator embed
55 changes: 55 additions & 0 deletions language/bazel/visibility/lang.go
@@ -0,0 +1,55 @@
package visibility

import (
"flag"
"strings"

"github.com/bazelbuild/bazel-gazelle/config"
"github.com/bazelbuild/bazel-gazelle/language"
"github.com/bazelbuild/bazel-gazelle/rule"
)

type visibilityExtension struct{}

// NewLanguage constructs a new language.Language modifying visibility.
func NewLanguage() language.Language {
return &visibilityExtension{}
}

// Kinds instructs gazelle to match any 'package' rule as BUILD files can only have one.
func (*visibilityExtension) Kinds() map[string]rule.KindInfo {
return map[string]rule.KindInfo{
"package": {
MatchAny: true,
MergeableAttrs: map[string]bool{
"default_visibility": true,
},
},
}
}

// Loads noops because there are no imports to add
func (*visibilityExtension) Loads() []rule.LoadInfo {
return nil
}

// GenerateRules does the hard work of setting the default_visibility if a config exists.
func (*visibilityExtension) GenerateRules(args language.GenerateArgs) language.GenerateResult {
res := language.GenerateResult{}
cfg := getVisConfig(args.Config)

if len(cfg.visibilityTargets) == 0 {
return res
}

r := rule.NewRule("package", "")
r.SetAttr("default_visibility", cfg.visibilityTargets)

res.Gen = append(res.Gen, r)
// we have to add a nil to Imports because there is length-matching validation with Gen.
res.Imports = append(res.Imports, nil)
return res
}

// Fix noop because there is nothing out there to fix yet
func (*visibilityExtension) Fix(c *config.Config, f *rule.File) {}
74 changes: 74 additions & 0 deletions language/bazel/visibility/lang_test.go
@@ -0,0 +1,74 @@
package visibility_test

import (
"fmt"
"testing"

"github.com/bazelbuild/bazel-gazelle/language/bazel/visibility"
"github.com/bazelbuild/bazel-gazelle/config"
"github.com/bazelbuild/bazel-gazelle/label"
"github.com/bazelbuild/bazel-gazelle/language"
"github.com/bazelbuild/bazel-gazelle/rule"
"github.com/stretchr/testify/require"
)

func TestNoopsBecauseILoveCoverage(t *testing.T) {
ext := visibility.NewLanguage()

ext.RegisterFlags(nil /* flagset */, "command", nil /* config */)
ext.Resolve(nil /* config */, nil /* RuleIndex */, nil /* RemoteCache */, nil /* Rule */, nil /* imports */, label.New("repo", "pkg", "name"))
ext.Fix(nil /* config */, nil /* File */)
require.Nil(t, ext.CheckFlags(nil /* flagset */, nil /* config */))
require.Nil(t, ext.Imports(nil /* rule */, nil /* rule */, nil /* file */))
require.Nil(t, ext.Embeds(nil /* rule */, label.New("repo", "pkg", "name")))
require.Nil(t, ext.Loads())
require.NotNil(t, ext.KnownDirectives())
require.NotNil(t, ext.Name())
}

func Test_NoDirective(t *testing.T) {
cfg := config.New()
file := rule.EmptyFile("path", "pkg")

ext := visibility.NewLanguage()
ext.Configure(cfg, "rel", file)
res := ext.GenerateRules(language.GenerateArgs{Config: cfg})

require.Len(t, res.Imports, 0)
require.Len(t, res.Gen, 0)
}

func Test_NewDirective(t *testing.T) {
testVis := "//src:__subpackages__"
cfg := config.New()
file, err := rule.LoadData("path", "pkg", []byte(fmt.Sprintf("# gazelle:default_visibility %s", testVis)))
require.Nil(t, err)

ext := visibility.NewLanguage()
ext.Configure(cfg, "rel", file)
res := ext.GenerateRules(language.GenerateArgs{Config: cfg})

require.Len(t, res.Gen, 1)
require.Len(t, res.Imports, 1)
require.Len(t, res.Gen[0].AttrStrings("default_visibility"), 1)
require.Equal(t, testVis, res.Gen[0].AttrStrings("default_visibility")[0])
}

func Test_ReplacementDirective(t *testing.T) {
testVis := "//src:__subpackages__"
cfg := config.New()
file, err := rule.LoadData("path", "pkg", []byte(fmt.Sprintf(`
# gazelle:default_visibility %s
package(default_visibility = "//not-src:__subpackages__")
`, testVis)))
require.Nil(t, err)

ext := visibility.NewLanguage()
ext.Configure(cfg, "rel", file)
res := ext.GenerateRules(language.GenerateArgs{Config: cfg})

require.Len(t, res.Gen, 1)
require.Len(t, res.Imports, 1)
require.Len(t, res.Gen[0].AttrStrings("default_visibility"), 1)
require.Equal(t, testVis, res.Gen[0].AttrStrings("default_visibility")[0])
35 changes: 35 additions & 0 deletions language/bazel/visibility/resolve.go
@@ -0,0 +1,35 @@
package visibility

import (
"flag"
"strings"

"github.com/bazelbuild/bazel-gazelle/config"
"github.com/bazelbuild/bazel-gazelle/label"
"github.com/bazelbuild/bazel-gazelle/repo"
"github.com/bazelbuild/bazel-gazelle/resolve"
"github.com/bazelbuild/bazel-gazelle/rule"
)

const (
_extName = "visibility_extension"
)

// Name returns the extension name.
func (*visibilityExtension) Name() string {
return _extName
}

// Imports noops because no imports are needed to leverage this functionality.
func (*visibilityExtension) Imports(c *config.Config, r *rule.Rule, f *rule.File) []resolve.ImportSpec {
return nil
}

// Embeds noops because we are not a traditional rule, and cannot be embedded.
func (*visibilityExtension) Embeds(r *rule.Rule, from label.Label) []label.Label {
return nil
}

// Resolve noops because we don't have deps=[] to resolve.
func (*visibilityExtension) Resolve(c *config.Config, ix *resolve.RuleIndex, rc *repo.RemoteCache, r *rule.Rule, imports interface{}, from label.Label) {
}

0 comments on commit a56b4c8

Please sign in to comment.