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
5 changes: 4 additions & 1 deletion .clj-kondo/config.edn
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
{: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}}}}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
.cpcache/
.ijwb/
.lsp/
.serena/
bazel-*
examples/gen_srcs_bench/src/
examples/gen_srcs_bench/hyperfine.json
.nrepl-port
target/
15 changes: 15 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@ 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")

# babashka binary for the Gazelle plugin's parser subprocess.
bazel_dep(name = "rules_multitool", version = "1.11.1")

multitool = use_extension("@rules_multitool//multitool:extension.bzl", "multitool")
multitool.hub(lockfile = "//:multitool.lock.json")
use_repo(multitool, "multitool")

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

Large diffs are not rendered by default.

66 changes: 66 additions & 0 deletions gazelle/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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",
],
data = [
"//src/rules_clojure:gazelle_server.bb",
"@multitool//tools/bb",
],
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",
"@rules_go//go/runfiles: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//resolve:go_default_library",
"@gazelle//rule:go_default_library",
],
)

gazelle_binary(
name = "gazelle_bin",
languages = [":gazelle_lib"],
visibility = ["//visibility:public"],
)

# Re-exposed filegroup for external/downstream consumers who want to
# reference the bb script directly. The actual data-dep that ships the
# script in gazelle_bin's runfiles is gazelle_lib's `data = [...]` above;
# resolveParserScript in configure.go reads it from runfiles via the
# rules_go runfiles library (which honours RUNFILES_DIR /
# RUNFILES_MANIFEST_FILE).
filegroup(
name = "gazelle_server_bb",
srcs = ["//src/rules_clojure:gazelle_server.bb"],
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"],
)
102 changes: 102 additions & 0 deletions gazelle/clojureconfig/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Package clojureconfig collects Gazelle directives for the Clojure plugin.
// Holds raw directive values per package with parent-fallback inheritance.
package clojureconfig

import (
"path"
"strings"
)

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

// Configs holds raw directive values per package with parent-fallback
// inheritance via Effective().
type Configs struct {
rels map[string]map[string]string
}

// New returns a Configs ready for use. The zero-value Configs is unsafe
// (nil inner map).
func New() *Configs {
return &Configs{rels: 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.rels[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 {
return cs.Effective(rel, ClojureEnabledDirective, "true") != "false"
}

// DepsRepo returns the deps repo tag for rel (default "@deps" at the root,
// overridable 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 absolute deps.edn path.
func (cs *Configs) DepsEdn() string {
return cs.Effective("", ClojureDepsEdn, "")
}

// Aliases returns the parsed root-level alias list.
func (cs *Configs) Aliases() []string {
return ParseAliases(cs.Effective("", ClojureAliases, ""))
}

// Set stores a raw directive value on `rel`. An empty value is dropped
// (a child config setting "" would otherwise shadow the parent).
func (cs *Configs) Set(rel, key, value string) {
if value == "" {
return
}
if _, ok := cs.rels[rel]; !ok {
cs.rels[rel] = map[string]string{}
}
cs.rels[rel][key] = value
}

// AllDirectives returns directive names recognised in BUILD-file comments.
func AllDirectives() []string {
return []string{
ClojureEnabledDirective,
ClojureDepsEdn,
ClojureDepsRepo,
ClojureAliases,
}
}

// ParseAliases splits a comma-separated alias string, trimming whitespace
// and dropping empties.
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
}
89 changes: 89 additions & 0 deletions gazelle/clojureconfig/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
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 := New()
cs.Set("", ClojureDepsRepo, "@root")
cs.Set("a", ClojureDepsRepo, "@a")
cs.Set("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 := New()
if got := cs.DepsRepo("anything"); got != "@deps" {
t.Errorf("DepsRepo on empty Configs = %q; want @deps default", got)
}
}

func TestExtensionEnabledFalseSticks(t *testing.T) {
cs := New()
if !cs.ExtensionEnabled("") {
t.Error("default ExtensionEnabled = false; want true")
}
cs.Set("a", ClojureEnabledDirective, "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 := New()
cs.Set("", ClojureAliases, "dev,test")
got := cs.Aliases()
if !reflect.DeepEqual(got, []string{"dev", "test"}) {
t.Errorf("Aliases() = %v; want [dev test]", got)
}
}

26 changes: 26 additions & 0 deletions gazelle/clojureparser/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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",
],
data = [
"//src/rules_clojure:gazelle_server.bb",
"@multitool//tools/bb",
],
embed = [":clojureparser"],
env = {
"GAZELLE_CLOJURE_BB": "$(rlocationpath @multitool//tools/bb)",
"GAZELLE_CLOJURE_PARSER": "$(rlocationpath //src/rules_clojure:gazelle_server.bb)",
},
deps = ["@rules_go//go/runfiles:go_default_library"],
)
Loading