Skip to content

Commit 20227cf

Browse files
committed
bazel: add go_stringer rule and Gazelle extension
### What does this PR do? - add `bazel/tools/stringer_wrapper.sh` + `sh_binary`: wrapper that puts the rules_go `go` binary on PATH and cds to the package directory before invoking stringer, so the generated header matches `//go:generate` output exactly - add `bazel/rules/go_stringer.bzl`: `go_stringer` Starlark macro wrapping `run_binary`; params: `name`, `src`, `type`, `gomod`, `output`, `trimprefix`, `linecomment`, `go_tags`, `**kwargs` - add `internal/tools/gazelle-stringer/`: Gazelle language extension that reads `//go:generate stringer` directives and emits `go_stringer` rules; wired into a custom `gazelle_binary` at `//:gazelle` - Bazelify `pkg/template` as proof of concept: BUILD.bazel files for the root package, `internal/fmtsort`, `text`, and `html`; the six `html` stringer targets, one `write_source_file` diff-test per target, and `go_library` targets; unexclude `pkg/template` from the root Gazelle exclusion list - update checked-in `*_string.go` files in `pkg/template/html`; those files were copied from the Go stdlib in #36024 and had never been regenerated in this repo; the diffs reflect a newer `golang.org/x/tools` version bundled with rules_go vs what the Go team used at the time ### Motivation Replaces manual `go generate` invocations with hermetic Bazel actions, making the build self-contained and stringer outputs verifiable via `bazel test`. ### Describe how you validated your changes - `bazel build //pkg/template/html:...` builds all six stringer targets and the `go_library` - `bazel test //pkg/template/...` passes six diff-tests (one per generated file) - `bazel run //:gazelle -- -mode=diff` produces no diff
1 parent 179e651 commit 20227cf

File tree

13 files changed

+331
-13
lines changed

13 files changed

+331
-13
lines changed

BUILD.bazel

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
load("@bazel_lib//lib:write_source_files.bzl", "write_source_file")
44
load("@bazel_skylib//rules:common_settings.bzl", "string_flag")
55
load("@bazel_skylib//rules:run_binary.bzl", "run_binary")
6-
load("@gazelle//:def.bzl", "gazelle")
6+
load("@gazelle//:def.bzl", "gazelle", "gazelle_binary")
77

88
package(default_visibility = ["//visibility:public"])
99

@@ -82,9 +82,19 @@ package(default_visibility = ["//visibility:public"])
8282
# gazelle:exclude tools
8383
# gazelle:exclude .gitlab
8484
# gazelle:prefix github.com/DataDog/datadog-agent
85+
gazelle_binary(
86+
name = "gazelle_bin",
87+
languages = [
88+
"@gazelle//language/go",
89+
"@gazelle//language/proto",
90+
"//bazel/rules/go_stringer:gazelle",
91+
],
92+
)
93+
8594
gazelle(
8695
name = "gazelle",
8796
args = ["-external=static"], # don't use the network: https://github.com/bazel-contrib/bazel-gazelle/issues/1385
97+
gazelle = ":gazelle_bin",
8898
)
8999

90100
# bazel run //:go -- ...
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
load("@rules_go//go:def.bzl", "go_library")
2+
load("@rules_shell//shell:sh_binary.bzl", "sh_binary")
3+
4+
sh_binary(
5+
name = "stringer_wrapper",
6+
srcs = ["stringer_wrapper.sh"],
7+
visibility = ["//visibility:public"],
8+
)
9+
10+
go_library(
11+
name = "gazelle",
12+
srcs = ["gazelle.go"],
13+
importpath = "github.com/DataDog/datadog-agent/bazel/rules/go_stringer",
14+
visibility = ["//visibility:public"],
15+
deps = [
16+
"@gazelle//language",
17+
"@gazelle//rule",
18+
],
19+
)

bazel/rules/go_stringer/defs.bzl

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
load("@bazel_lib//lib:run_binary.bzl", "run_binary")
2+
load("@bazel_lib//lib:write_source_files.bzl", "write_source_file")
3+
4+
_ENV = {
5+
"GO_BIN": "$(execpath @rules_go//go)",
6+
"STRINGER_BIN": "$(execpath @org_golang_x_tools//cmd/stringer)",
7+
}
8+
9+
def _impl(name, mod, output, src, type, go_tags, linecomment, trimprefix, visibility):
10+
out_new = output + ".new"
11+
args = ["-type", type]
12+
if output != type.split(",")[0].lower() + "_string.go":
13+
args += ["-output", output]
14+
if go_tags:
15+
args += ["-tags", go_tags]
16+
if linecomment:
17+
args.append("-linecomment")
18+
if trimprefix:
19+
args += ["-trimprefix", trimprefix]
20+
run_binary(
21+
name = name + "_gen",
22+
tool = "//bazel/rules/go_stringer:stringer_wrapper",
23+
srcs = [src, "@rules_go//go", "@org_golang_x_tools//cmd/stringer", mod],
24+
outs = [out_new],
25+
args = args,
26+
env = dict(
27+
_ENV,
28+
PKG_SRC = "$(execpath " + str(src) + ")",
29+
STRINGER_OUT = "$(execpath " + out_new + ")",
30+
),
31+
visibility = ["//visibility:private"],
32+
)
33+
native.exports_files([output])
34+
write_source_file(name = name, in_file = ":" + name + "_gen", out_file = output, check_that_out_file_exists = False, visibility = visibility)
35+
36+
go_stringer = macro(
37+
implementation = _impl,
38+
attrs = {
39+
"mod": attr.label(mandatory = True, configurable = False),
40+
"output": attr.string(mandatory = True, configurable = False),
41+
"src": attr.label(mandatory = True, configurable = False),
42+
"type": attr.string(mandatory = True, configurable = False),
43+
"go_tags": attr.string(configurable = False),
44+
"linecomment": attr.bool(default = False, configurable = False),
45+
"trimprefix": attr.string(configurable = False),
46+
},
47+
)

bazel/rules/go_stringer/gazelle.go

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
// Package stringer is a Gazelle extension that generates go_stringer rules
2+
// from //go:generate stringer directives in Go source files.
3+
package stringer
4+
5+
import (
6+
"bufio"
7+
"flag"
8+
"io"
9+
"os"
10+
"path/filepath"
11+
"strings"
12+
13+
"github.com/bazelbuild/bazel-gazelle/language"
14+
"github.com/bazelbuild/bazel-gazelle/rule"
15+
)
16+
17+
// NewLanguage is called by Gazelle to instantiate this extension.
18+
func NewLanguage() language.Language {
19+
return &lang{}
20+
}
21+
22+
type lang struct {
23+
language.BaseLang
24+
}
25+
26+
func (*lang) Name() string { return "stringer" }
27+
28+
func (*lang) Kinds() map[string]rule.KindInfo {
29+
return map[string]rule.KindInfo{
30+
"go_stringer": {
31+
MatchAttrs: []string{"output"},
32+
NonEmptyAttrs: map[string]bool{"src": true, "type": true, "mod": true, "output": true},
33+
MergeableAttrs: map[string]bool{},
34+
},
35+
}
36+
}
37+
38+
func (*lang) Loads() []rule.LoadInfo {
39+
return []rule.LoadInfo{{
40+
Name: "//bazel/rules/go_stringer:defs.bzl",
41+
Symbols: []string{"go_stringer"},
42+
}}
43+
}
44+
45+
func (*lang) GenerateRules(args language.GenerateArgs) language.GenerateResult {
46+
mod := findGomod(args.Config.RepoRoot, args.Dir)
47+
var rules []*rule.Rule
48+
49+
for _, f := range args.RegularFiles {
50+
if !strings.HasSuffix(f, ".go") {
51+
continue
52+
}
53+
directives, err := parseFile(filepath.Join(args.Dir, f))
54+
if err != nil {
55+
continue
56+
}
57+
for _, d := range directives {
58+
out := d.output
59+
if out == "" {
60+
out = strings.ToLower(strings.SplitN(d.typ, ",", 2)[0]) + "_string.go"
61+
}
62+
r := rule.NewRule("go_stringer", strings.TrimSuffix(out, ".go"))
63+
r.SetAttr("src", f)
64+
r.SetAttr("type", d.typ)
65+
r.SetAttr("mod", mod)
66+
r.SetAttr("output", out)
67+
if d.trimprefix != "" {
68+
r.SetAttr("trimprefix", d.trimprefix)
69+
}
70+
if d.linecomment {
71+
r.SetAttr("linecomment", true)
72+
}
73+
if d.tags != "" {
74+
r.SetAttr("go_tags", d.tags)
75+
}
76+
rules = append(rules, r)
77+
}
78+
}
79+
if len(rules) == 0 {
80+
return language.GenerateResult{}
81+
}
82+
return language.GenerateResult{
83+
Gen: rules,
84+
Imports: make([]interface{}, len(rules)),
85+
}
86+
}
87+
88+
type directive struct {
89+
typ string
90+
output string
91+
trimprefix string
92+
linecomment bool
93+
tags string
94+
}
95+
96+
func parseFile(path string) ([]directive, error) {
97+
f, err := os.Open(path)
98+
if err != nil {
99+
return nil, err
100+
}
101+
defer f.Close()
102+
103+
var result []directive
104+
scanner := bufio.NewScanner(f)
105+
for scanner.Scan() {
106+
line := scanner.Text()
107+
if !strings.HasPrefix(line, "//go:generate ") {
108+
continue
109+
}
110+
if d, ok := parseDirective(strings.TrimPrefix(line, "//go:generate ")); ok {
111+
result = append(result, d)
112+
}
113+
}
114+
return result, scanner.Err()
115+
}
116+
117+
func parseDirective(s string) (directive, bool) {
118+
fields := strings.Fields(s)
119+
if len(fields) == 0 {
120+
return directive{}, false
121+
}
122+
var args []string
123+
switch {
124+
case isStringerCmd(fields[0]):
125+
args = fields[1:]
126+
case fields[0] == "go" && len(fields) >= 3 && fields[1] == "run" && isStringerCmd(fields[2]):
127+
args = fields[3:]
128+
default:
129+
return directive{}, false
130+
}
131+
132+
fs := flag.NewFlagSet("stringer", flag.ContinueOnError)
133+
fs.SetOutput(io.Discard)
134+
typ := fs.String("type", "", "")
135+
output := fs.String("output", "", "")
136+
trimprefix := fs.String("trimprefix", "", "")
137+
linecomment := fs.Bool("linecomment", false, "")
138+
tags := fs.String("tags", "", "")
139+
if err := fs.Parse(args); err != nil || *typ == "" {
140+
return directive{}, false
141+
}
142+
return directive{
143+
typ: *typ,
144+
output: *output,
145+
trimprefix: *trimprefix,
146+
linecomment: *linecomment,
147+
tags: *tags,
148+
}, true
149+
}
150+
151+
func isStringerCmd(s string) bool {
152+
return s == "stringer" ||
153+
strings.HasSuffix(s, "/stringer") ||
154+
strings.HasSuffix(s, "/cmd/stringer")
155+
}
156+
157+
func findGomod(repoRoot, dir string) string {
158+
for d := dir; ; {
159+
if _, err := os.Stat(filepath.Join(d, "go.mod")); err == nil {
160+
rel, err := filepath.Rel(repoRoot, d)
161+
if err != nil || rel == "." {
162+
return "//:go.mod"
163+
}
164+
return "//" + filepath.ToSlash(rel) + ":go.mod"
165+
}
166+
parent := filepath.Dir(d)
167+
if parent == d {
168+
return "//:go.mod"
169+
}
170+
d = parent
171+
}
172+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
EXECROOT="$PWD"
4+
export PATH="$EXECROOT/$(dirname "$GO_BIN"):$PATH"
5+
export GOWORK=off
6+
export HOME=/tmp
7+
8+
cd "$EXECROOT/$(dirname "$PKG_SRC")"
9+
"$EXECROOT/$STRINGER_BIN" "$@"
10+
mv "$(basename "$STRINGER_OUT" .new)" "$EXECROOT/$STRINGER_OUT"

pkg/template/BUILD.bazel

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
load("@rules_go//go:def.bzl", "go_library")
22

3+
exports_files(
4+
["go.mod"],
5+
visibility = ["//pkg/template:__subpackages__"],
6+
)
7+
38
go_library(
49
name = "template",
510
srcs = ["docs.go"],

pkg/template/html/BUILD.bazel

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,53 @@
11
load("@rules_go//go:def.bzl", "go_library")
2+
load("//bazel/rules/go_stringer:defs.bzl", "go_stringer")
3+
4+
go_stringer(
5+
name = "state_string",
6+
src = "context.go",
7+
mod = "//pkg/template:go.mod",
8+
output = "state_string.go",
9+
type = "state",
10+
)
11+
12+
go_stringer(
13+
name = "delim_string",
14+
src = "context.go",
15+
mod = "//pkg/template:go.mod",
16+
output = "delim_string.go",
17+
type = "delim",
18+
)
19+
20+
go_stringer(
21+
name = "urlpart_string",
22+
src = "context.go",
23+
mod = "//pkg/template:go.mod",
24+
output = "urlpart_string.go",
25+
type = "urlPart",
26+
)
27+
28+
go_stringer(
29+
name = "jsctx_string",
30+
src = "context.go",
31+
mod = "//pkg/template:go.mod",
32+
output = "jsctx_string.go",
33+
type = "jsCtx",
34+
)
35+
36+
go_stringer(
37+
name = "element_string",
38+
src = "context.go",
39+
mod = "//pkg/template:go.mod",
40+
output = "element_string.go",
41+
type = "element",
42+
)
43+
44+
go_stringer(
45+
name = "attr_string",
46+
src = "context.go",
47+
mod = "//pkg/template:go.mod",
48+
output = "attr_string.go",
49+
type = "attr",
50+
)
251

352
go_library(
453
name = "html",

pkg/template/html/attr_string.go

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/template/html/delim_string.go

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/template/html/element_string.go

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)