Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .clj-kondo/config.edn
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
{:linters {:unused-binding {:exclude-destructured-keys-in-fn-args true
:exclude-destructured-as true}}}
:exclude-destructured-as true}
;; Tests in this repo use `(:require [clojure.test :refer :all])`
;; for the conventional shorthand; silence kondo's default warning.
:refer-all {:exclude #{clojure.test}}}
:lint-as {rules-clojure.gazelle-server-test/with-temp-dir clojure.core/with-open}}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ bazel-*
examples/gen_srcs_bench/src/
examples/gen_srcs_bench/hyperfine.json
.nrepl-port
target/
8 changes: 8 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ bazel_dep(name = "rules_jvm_external", version = "6.8")

bazel_dep(name = "buildifier_prebuilt", version = "8.5.1.2", dev_dependency = True)

# Go + Gazelle for the Clojure Gazelle plugin (//gazelle).
bazel_dep(name = "rules_go", version = "0.60.0")
bazel_dep(name = "gazelle", version = "0.47.0")

go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps")
go_deps.from_file(go_mod = "//gazelle:go.mod")
use_repo(go_deps, "com_github_bazelbuild_buildtools")

maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
maven.install(
name = "maven_deps",
Expand Down
527 changes: 525 additions & 2 deletions MODULE.bazel.lock

Large diffs are not rendered by default.

48 changes: 48 additions & 0 deletions gazelle/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
load("@gazelle//:def.bzl", "gazelle_binary")
load("@rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "gazelle_lib",
srcs = [
"configure.go",
"generate.go",
"lang.go",
"resolve.go",
],
importpath = "github.com/griffinbank/rules_clojure/gazelle",
visibility = ["//visibility:public"],
deps = [
"//gazelle/clojureconfig",
"//gazelle/clojureparser",
"@gazelle//config:go_default_library",
"@gazelle//label:go_default_library",
"@gazelle//language:go_default_library",
"@gazelle//repo:go_default_library",
"@gazelle//resolve:go_default_library",
"@gazelle//rule:go_default_library",
],
)

go_test(
name = "gazelle_test",
srcs = [
"configure_test.go",
"generate_test.go",
"resolve_test.go",
],
embed = [":gazelle_lib"],
deps = [
"//gazelle/clojureconfig",
"//gazelle/clojureparser",
"@com_github_bazelbuild_buildtools//build:go_default_library",
"@gazelle//config:go_default_library",
"@gazelle//label:go_default_library",
"@gazelle//rule:go_default_library",
],
)

gazelle_binary(
name = "gazelle_bin",
languages = [":gazelle_lib"],
visibility = ["//visibility:public"],
)
14 changes: 14 additions & 0 deletions gazelle/clojureconfig/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
load("@rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "clojureconfig",
srcs = ["config.go"],
importpath = "github.com/griffinbank/rules_clojure/gazelle/clojureconfig",
visibility = ["//gazelle:__subpackages__"],
)

go_test(
name = "clojureconfig_test",
srcs = ["config_test.go"],
embed = [":clojureconfig"],
)
132 changes: 132 additions & 0 deletions gazelle/clojureconfig/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Package clojureconfig collects Gazelle directives for the Clojure plugin.
// It holds raw directive values per package (rel → kv-map) and supports the
// parent-fallback walk Gazelle's per-package config model implies. Semantic
// interpretation of resolved values — basis assembly, label generation,
// effective rule shape — belongs in the Clojure server; the trivial value-
// string normalization needed for directive consumption (alias parsing,
// trim-empty) lives here because every Go-side caller needs it.
package clojureconfig

import (
"path"
"strings"
)

// Directive name constants used in BUILD file comments.
const (
ClojureExtensionDirective = "clojure_enabled"
ClojureDepsEdn = "clojure_deps_edn"
ClojureDepsRepo = "clojure_deps_repo"
ClojureAliases = "clojure_aliases"

// rootModuleName is auto-discovered from MODULE.bazel; it isn't a
// user-facing directive but is stored under the same map.
rootModuleName = "_root_module_name"
)

// Configs maps package-relative paths to their raw directive values.
// Lookup goes through Effective() which walks ancestors for inheritance.
type Configs map[string]map[string]string

// Effective returns the value for `key` at `rel`, falling back to ancestor
// rels (parent dirs) and finally `dflt` when no override was set.
func (cs Configs) Effective(rel, key, dflt string) string {
for {
if v, ok := cs[rel][key]; ok {
return v
}
if rel == "" {
return dflt
}
rel = path.Dir(rel)
if rel == "." {
rel = ""
}
}
}

// ExtensionEnabled returns whether the Clojure extension is active for rel,
// honouring per-package `# gazelle:clojure_enabled false` overrides.
// Defaults to true at the root.
func (cs Configs) ExtensionEnabled(rel string) bool {
switch cs.Effective(rel, ClojureExtensionDirective, "true") {
case "false":
return false
default:
return true
}
}

// DepsRepo returns the deps repo tag for rel — default "@deps" at the root,
// overridden by `# gazelle:clojure_deps_repo @other_deps` in any ancestor.
func (cs Configs) DepsRepo(rel string) string {
return cs.Effective(rel, ClojureDepsRepo, "@deps")
}

// DepsEdn returns the workspace-relative deps.edn path captured at the root
// during the initial Configure pass. Auto-discovered if no directive set.
func (cs Configs) DepsEdn() string {
return cs.Effective("", ClojureDepsEdn, "")
}

// RootModuleName returns the bzlmod module(name=...) value, captured during
// the root Configure pass by reading MODULE.bazel. Used to canonicalize
// self-referencing labels (`@<root>//foo` → `@@//foo`) so generated rules
// agree with the apparent-name view Bazel exposes to consumers.
func (cs Configs) RootModuleName() string {
return cs.Effective("", rootModuleName, "")
}

// SetRootModuleName stashes the discovered bzlmod root module name on the
// root config.
func (cs Configs) SetRootModuleName(name string) {
if _, ok := cs[""]; !ok {
cs[""] = map[string]string{}
}
cs[""][rootModuleName] = name
}

// Aliases returns the parsed alias list from the root-level
// `# gazelle:clojure_aliases` directive. Per-package alias overrides are
// not supported (aliases feed the basis, which is computed once at init).
func (cs Configs) Aliases() []string {
return ParseAliases(cs.Effective("", ClojureAliases, ""))
}

// Set stores a raw directive value on `rel`. Empty values are treated as
// "unset" so a child config doesn't accidentally override its parent with
// the zero value — Effective() falls through to the next ancestor.
func (cs Configs) Set(rel, key, value string) {
if value == "" {
return
}
if _, ok := cs[rel]; !ok {
cs[rel] = map[string]string{}
}
cs[rel][key] = value
}

// AllDirectives returns the directive names Gazelle should recognize from
// `# gazelle:<name> <value>` comments in BUILD files.
func AllDirectives() []string {
return []string{
ClojureExtensionDirective,
ClojureDepsEdn,
ClojureDepsRepo,
ClojureAliases,
}
}

// ParseAliases splits a comma-separated alias string, trims whitespace,
// and drops empty entries. strings.Split("", ",") returns [""], and a
// naive split of "dev, test" leaves a leading space on the second entry
// — both yield malformed keywords on the Clojure side.
func ParseAliases(s string) []string {
out := []string{}
for _, raw := range strings.Split(s, ",") {
if trimmed := strings.TrimSpace(raw); trimmed != "" {
out = append(out, trimmed)
}
}
return out
}
97 changes: 97 additions & 0 deletions gazelle/clojureconfig/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package clojureconfig

import (
"reflect"
"testing"
)

func TestParseAliases(t *testing.T) {
cases := []struct {
name string
in string
want []string
}{
{name: "empty", in: "", want: []string{}},
{name: "single", in: "dev", want: []string{"dev"}},
{name: "two", in: "dev,test", want: []string{"dev", "test"}},
{name: "trims whitespace", in: " dev , test ", want: []string{"dev", "test"}},
{name: "drops empty entries", in: ",,dev,,test,", want: []string{"dev", "test"}},
{name: "all empty", in: ",,,", want: []string{}},
{name: "colon-prefixed kept verbatim", in: ":dev", want: []string{":dev"}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := ParseAliases(tc.in)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("ParseAliases(%q) = %v; want %v", tc.in, got, tc.want)
}
})
}
}

func TestEffectiveWalksParentChain(t *testing.T) {
cs := Configs{
"": {ClojureDepsRepo: "@root"},
"a": {ClojureDepsRepo: "@a"},
"a/b/c": {ClojureDepsRepo: "@deep"},
}
cases := map[string]string{
"": "@root",
"a": "@a",
"a/b": "@a", // a/b unset → a
"a/b/c": "@deep", // exact match
"a/b/c/d": "@deep", // child of c → deep
"x/y/z": "@root", // unrelated → root default
}
for rel, want := range cases {
t.Run(rel, func(t *testing.T) {
got := cs.DepsRepo(rel)
if got != want {
t.Errorf("DepsRepo(%q) = %q; want %q", rel, got, want)
}
})
}
}

func TestEffectiveDefaultsWhenNothingSet(t *testing.T) {
cs := Configs{}
if got := cs.DepsRepo("anything"); got != "@deps" {
t.Errorf("DepsRepo on empty Configs = %q; want @deps default", got)
}
}

func TestExtensionEnabledFalseSticks(t *testing.T) {
cs := Configs{}
if !cs.ExtensionEnabled("") {
t.Error("default ExtensionEnabled = false; want true")
}
cs.Set("a", ClojureExtensionDirective, "false")
if cs.ExtensionEnabled("a") {
t.Error("ExtensionEnabled(a) after false override = true")
}
// Child inherits parent's false.
if cs.ExtensionEnabled("a/b") {
t.Error("child ExtensionEnabled = true; want inherited false")
}
// Sibling unaffected.
if !cs.ExtensionEnabled("other") {
t.Error("sibling ExtensionEnabled = false; want default true")
}
}

func TestAliasesParsesRootDirective(t *testing.T) {
cs := Configs{}
cs.Set("", ClojureAliases, "dev,test")
got := cs.Aliases()
if !reflect.DeepEqual(got, []string{"dev", "test"}) {
t.Errorf("Aliases() = %v; want [dev test]", got)
}
}

func TestSetRootModuleName(t *testing.T) {
cs := Configs{}
cs.SetRootModuleName("foo")
if got := cs.RootModuleName(); got != "foo" {
t.Errorf("RootModuleName = %q; want foo", got)
}
}
17 changes: 17 additions & 0 deletions gazelle/clojureparser/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
load("@rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "clojureparser",
srcs = ["parser.go"],
importpath = "github.com/griffinbank/rules_clojure/gazelle/clojureparser",
visibility = ["//gazelle:__subpackages__"],
)

go_test(
name = "clojureparser_test",
srcs = [
"lifecycle_test.go",
"parser_test.go",
],
embed = [":clojureparser"],
)
Loading