From fd09b82bdd2b0f5a9d660a88f0d96b7954090b8f Mon Sep 17 00:00:00 2001 From: asp24 <488588+asp24@users.noreply.github.com> Date: Tue, 26 May 2026 16:41:28 +0200 Subject: [PATCH 01/12] Add --enable-pass and --disable-pass CLI flags for optional pass control Introduce di.OptionalPass (extends Pass with RunByDefault) so custom generator binaries can register passes that users may toggle via CLI flags. Update cmd.Run and cmd.MustRun to accept []di.OptionalPass, add PassConfig with resolvePasses logic, and update the custom-pass example and all documentation accordingly. --- AGENTS.md | 4 +- README.md | 6 ++ cmd/cli.go | 6 +- cmd/config.go | 2 + cmd/pass_config.go | 46 +++++++++++++++ cmd/string_set_flag.go | 31 ++++++++++ config.go | 5 ++ doc/LLM.md | 2 +- doc/custom-passes.md | 59 +++++++++++++++---- doc/spec/cli.md | 2 + examples/custom-pass/README.md | 16 +++-- .../custom-pass/internal/di/autotag_pass.go | 4 ++ examples/custom-pass/internal/di/slog_pass.go | 4 ++ examples/custom-pass/tools/gendi/main.go | 2 +- 14 files changed, 164 insertions(+), 25 deletions(-) create mode 100644 cmd/pass_config.go create mode 100644 cmd/string_set_flag.go diff --git a/AGENTS.md b/AGENTS.md index 191593e..c2908b6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -230,6 +230,8 @@ type MyPass struct{} func (p *MyPass) Name() string { return "my-pass" } +func (p *MyPass) RunByDefault() bool { return true } + func (p *MyPass) Process(cfg *di.Config) (*di.Config, error) { // Mutate cfg (add services, modify args, etc.) return cfg, nil @@ -240,7 +242,7 @@ Use in custom generator: ```go // tools/gendi/main.go func main() { - passes := []di.Pass{&MyPass{}} + passes := []di.OptionalPass{&MyPass{}} cmd.Run(flag.CommandLine, passes) } ``` diff --git a/README.md b/README.md index 97cf56d..2df3644 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,10 @@ Flags: --container string Container struct name (default: "Container") --strict Enable strict validation (default: true) --build-tags string Build tags for generated file + --enable-pass string + Enable an optional compiler pass (repeatable) + --disable-pass string + Disable an optional compiler pass (repeatable) --verbose Enable verbose logging ``` @@ -159,6 +163,8 @@ type AutoTagPass struct{} func (p *AutoTagPass) Name() string { return "auto-tag" } +func (p *AutoTagPass) RunByDefault() bool { return true } + func (p *AutoTagPass) Process(cfg *di.Config) (*di.Config, error) { for id, svc := range cfg.Services { if strings.HasSuffix(id, ".handler") { diff --git a/cmd/cli.go b/cmd/cli.go index 3802500..f24d5df 100644 --- a/cmd/cli.go +++ b/cmd/cli.go @@ -57,7 +57,7 @@ func Generate(cfg Config, passes []di.Pass) error { } // Run executes the full gendi workflow with optional compiler passes -func Run(flags *flag.FlagSet, passes []di.Pass) error { +func Run(flags *flag.FlagSet, passes []di.OptionalPass) error { var cfg Config cfg.RegisterFlags(flags) @@ -65,7 +65,7 @@ func Run(flags *flag.FlagSet, passes []di.Pass) error { return fmt.Errorf("parse flags: %w", err) } - return Generate(cfg, passes) + return Generate(cfg, cfg.Passes.resolvePasses(passes)) } func PrintErrorAndExit(err error) { @@ -79,6 +79,6 @@ func PrintErrorAndExit(err error) { os.Exit(1) } -func MustRun(flags *flag.FlagSet, passes []di.Pass) { +func MustRun(flags *flag.FlagSet, passes []di.OptionalPass) { PrintErrorAndExit(Run(flags, passes)) } diff --git a/cmd/config.go b/cmd/config.go index 7fce479..064f01f 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -11,6 +11,7 @@ import ( type Config struct { ConfigPath string Options pipeline.Options + Passes PassConfig } func (c *Config) RegisterFlags(flags *flag.FlagSet) { @@ -21,6 +22,7 @@ func (c *Config) RegisterFlags(flags *flag.FlagSet) { flags.BoolVar(&c.Options.Strict, "strict", true, "Enable strict validation") flags.StringVar(&c.Options.BuildTags, "build-tags", "", "Go build tags") flags.BoolVar(&c.Options.Verbose, "verbose", false, "Verbose logging") + c.Passes.RegisterFlags(flags) } // Finalize validates and finalizes the configuration diff --git a/cmd/pass_config.go b/cmd/pass_config.go new file mode 100644 index 0000000..7c93bb5 --- /dev/null +++ b/cmd/pass_config.go @@ -0,0 +1,46 @@ +package cmd + +import ( + "flag" + + di "github.com/asp24/gendi" +) + +// PassConfig holds enabled/disabled pass configuration. +type PassConfig struct { + Enabled map[string]struct{} // Pass names to enable + Disabled map[string]struct{} // Pass names to disable +} + +// resolvePasses builds the final list of passes from the passed-in list, +// applying enable/disable filtering and always including enabled-by-default passes. +func (pc *PassConfig) resolvePasses(passes []di.OptionalPass) []di.Pass { + result := make([]di.Pass, 0, len(passes)) + included := make(map[string]struct{}, len(passes)) + for _, p := range passes { + name := p.Name() + if _, ok := included[name]; ok { + continue + } + + _, enabled := pc.Enabled[name] + if !p.RunByDefault() && !enabled { + continue + } + + if _, disabled := pc.Disabled[name]; disabled { + continue + } + + result = append(result, p) + included[name] = struct{}{} + } + + return result +} + +// RegisterFlags adds pass enable/disable flags to the flag set. +func (pc *PassConfig) RegisterFlags(flags *flag.FlagSet) { + flags.Var(&stringSetFlag{values: &pc.Enabled}, "enable-pass", "Enable a specific compiler pass (can be specified multiple times)") + flags.Var(&stringSetFlag{values: &pc.Disabled}, "disable-pass", "Disable a specific compiler pass (can be specified multiple times)") +} diff --git a/cmd/string_set_flag.go b/cmd/string_set_flag.go new file mode 100644 index 0000000..cc25a7c --- /dev/null +++ b/cmd/string_set_flag.go @@ -0,0 +1,31 @@ +package cmd + +import ( + "sort" + "strings" +) + +// stringSetFlag is a flag.Value that collects multiple values into a set. +type stringSetFlag struct { + values *map[string]struct{} +} + +func (f *stringSetFlag) String() string { + if f.values == nil || *f.values == nil { + return "" + } + values := make([]string, 0, len(*f.values)) + for value := range *f.values { + values = append(values, value) + } + sort.Strings(values) + return strings.Join(values, ",") +} + +func (f *stringSetFlag) Set(s string) error { + if *f.values == nil { + *f.values = make(map[string]struct{}) + } + (*f.values)[s] = struct{}{} + return nil +} diff --git a/config.go b/config.go index 0d5676c..92130f0 100644 --- a/config.go +++ b/config.go @@ -13,6 +13,11 @@ type Pass interface { Process(cfg *Config) (*Config, error) } +type OptionalPass interface { + Pass + RunByDefault() bool +} + // ApplyPasses applies compiler passes sequentially to the config. // Each pass receives the result of the previous pass. func ApplyPasses(cfg *Config, passes []Pass) (*Config, error) { diff --git a/doc/LLM.md b/doc/LLM.md index 4924661..c292013 100644 --- a/doc/LLM.md +++ b/doc/LLM.md @@ -8,7 +8,7 @@ Short, stable facts for tooling and assistants. go tool gendi --config=gendi.yaml --out=./di --pkg=di ``` -Flags: `--config`, `--out`, `--pkg`, `--container`, `--strict`, `--build-tags`, `--verbose`. +Flags: `--config`, `--out`, `--pkg`, `--container`, `--strict`, `--build-tags`, `--enable-pass`, `--disable-pass`, `--verbose`. ## YAML Syntax diff --git a/doc/custom-passes.md b/doc/custom-passes.md index 2ce21a6..4ef8fd3 100644 --- a/doc/custom-passes.md +++ b/doc/custom-passes.md @@ -6,6 +6,7 @@ Compiler passes transform configuration before code generation, enabling project - [Overview](#overview) - [Pass Interface](#pass-interface) +- [Optional CLI Passes](#optional-cli-passes) - [Creating a Pass](#creating-a-pass) - [Building a Custom Generator](#building-a-custom-generator) - [Common Use Cases](#common-use-cases) @@ -66,6 +67,28 @@ type Pass interface { - Returns an error if transformation fails - **Must not modify the input config** (create a copy if needed) +## Optional CLI Passes + +Custom generator binaries built with `cmd.Run` register optional passes. Optional passes implement `Pass` plus `RunByDefault`: + +```go +package di + +type OptionalPass interface { + Pass + RunByDefault() bool +} +``` + +`RunByDefault` controls how the pass participates in CLI filtering: + +- Return `true` to run the pass unless the user passes `--disable-pass=`. +- Return `false` to skip the pass unless the user passes `--enable-pass=`. +- Pass names come from `Name()`. +- If the same pass name is registered more than once, only the first included pass runs. + +Use `di.Pass` when calling `di.ApplyPasses` or `cmd.Generate` directly. Use `di.OptionalPass` when registering passes with `cmd.Run` or `cmd.MustRun`. + ## Creating a Pass ### Basic Pass Structure @@ -85,6 +108,10 @@ func (p *AutoTagPass) Name() string { return "auto-tag" } +func (p *AutoTagPass) RunByDefault() bool { + return true +} + func (p *AutoTagPass) Process(cfg *di.Config) (*di.Config, error) { // Transform cfg // Return modified config or error @@ -130,7 +157,7 @@ func (p *MyPass) Process(cfg *di.Config) (*di.Config, error) { ```go func (p *MyPass) Process(cfg *di.Config) (*di.Config, error) { cfg.Services["new_service"] = di.Service{ - Constructor: &di.Constructor{ + Constructor: di.Constructor{ Func: "github.com/myapp.NewService", Args: []di.Argument{ {Kind: di.ArgLiteral, Literal: di.NewStringLiteral("value")}, @@ -191,7 +218,7 @@ import ( func main() { // Define custom compiler passes - customPasses := []di.Pass{ + customPasses := []di.OptionalPass{ &passes.AutoTagPass{}, &passes.ValidationPass{}, } @@ -213,6 +240,12 @@ go build -o bin/gendi ./tools/gendi # Run custom generator ./bin/gendi --config=gendi.yaml --out=./di --pkg=di +# Disable a default-enabled optional pass +./bin/gendi --config=gendi.yaml --out=./di --pkg=di --disable-pass=auto-tag + +# Enable a default-disabled optional pass +./bin/gendi --config=gendi.yaml --out=./di --pkg=di --enable-pass=validation + # Or use go run go run ./tools/gendi --config=gendi.yaml --out=./di --pkg=di ``` @@ -278,13 +311,11 @@ func (p *LoggingPass) Process(cfg *di.Config) (*di.Config, error) { } // Add logger as first argument - if svc.Constructor != nil { - svc.Constructor.Args = append( - []di.Argument{{Kind: di.ArgServiceRef, Value: "logger"}}, - svc.Constructor.Args..., - ) - cfg.Services[id] = svc - } + svc.Constructor.Args = append( + []di.Argument{{Kind: di.ArgServiceRef, Value: "logger"}}, + svc.Constructor.Args..., + ) + cfg.Services[id] = svc } return cfg, nil @@ -417,7 +448,7 @@ type AutoTagPass struct{} Return descriptive errors: ```go -if svc.Constructor == nil { +if svc.Constructor.Func == "" && svc.Constructor.Method == "" { return nil, fmt.Errorf( "service %q: missing constructor (required by auto-tag pass)", id, @@ -460,7 +491,7 @@ func TestAutoTagPass(t *testing.T) { cfg := &di.Config{ Services: map[string]di.Service{ "home.handler": { - Constructor: &di.Constructor{ + Constructor: di.Constructor{ Func: "app.NewHomeHandler", }, }, @@ -493,6 +524,8 @@ customPasses := []di.Pass{ } ``` +If these passes are registered with `cmd.Run`, use `[]di.OptionalPass` and implement `RunByDefault` on each pass. + ## Complete Example See [examples/custom-pass](../examples/custom-pass) for a production-ready implementation featuring: @@ -511,7 +544,7 @@ func (p *ChannelLoggerPass) Name() string { func (p *ChannelLoggerPass) Process(cfg *di.Config) (*di.Config, error) { for id, svc := range cfg.Services { // Only process method constructors - if svc.Constructor == nil || svc.Constructor.Method == "" { + if svc.Constructor.Method == "" { continue } @@ -587,7 +620,7 @@ type Config struct { // Service represents a service definition type Service struct { Type string - Constructor *Constructor + Constructor Constructor Alias string Shared bool Public bool diff --git a/doc/spec/cli.md b/doc/spec/cli.md index 45e4491..8f831cd 100644 --- a/doc/spec/cli.md +++ b/doc/spec/cli.md @@ -16,6 +16,8 @@ gendi | `--container` | Container struct name | | `--strict` | Enable strict validation (default: true) | | `--build-tags` | Go build tags | +| `--enable-pass` | Enable an optional compiler pass by name; repeat for multiple passes | +| `--disable-pass` | Disable an optional compiler pass by name; repeat for multiple passes | | `--verbose` | Verbose logging | ## go:generate diff --git a/examples/custom-pass/README.md b/examples/custom-pass/README.md index b2ce855..c0201bc 100644 --- a/examples/custom-pass/README.md +++ b/examples/custom-pass/README.md @@ -68,7 +68,7 @@ This pass demonstrates **variadic function support** - `slog.Logger.With(args .. ### 1. Define Custom Passes -Implement the `di.Pass` interface: +Implement the `di.OptionalPass` interface for passes registered with `cmd.Run`: ```go type SLogPass struct{} @@ -77,6 +77,10 @@ func (s *SLogPass) Name() string { return "slog" } +func (s *SLogPass) RunByDefault() bool { + return true +} + func (s *SLogPass) Process(cfg *di.Config) (*di.Config, error) { // Transform config and return modified version return cfg, nil @@ -88,15 +92,12 @@ func (s *SLogPass) Process(cfg *di.Config) (*di.Config, error) { `tools/gendi/main.go`: ```go func main() { - passes := []gendi.Pass{ + passes := []gendi.OptionalPass{ &di.AutoTagPass{}, &di.SLogPass{}, } - if err := cmd.Run(flag.CommandLine, passes); err != nil { - fmt.Fprintf(os.Stderr, "%v\n", err) - os.Exit(1) - } + cmd.MustRun(flag.CommandLine, passes) } ``` @@ -134,6 +135,9 @@ The `cmd/main.go` includes a go:generate directive: # Generate the container with custom passes go run ./tools/gendi --config=./cmd/gendi.yaml --out=./cmd --pkg=main +# Disable a default-enabled optional pass +go run ./tools/gendi --config=./cmd/gendi.yaml --out=./cmd --pkg=main --disable-pass=slog + # Run the demo go run ./cmd/*.go ``` diff --git a/examples/custom-pass/internal/di/autotag_pass.go b/examples/custom-pass/internal/di/autotag_pass.go index 82eb7c8..f4ad959 100644 --- a/examples/custom-pass/internal/di/autotag_pass.go +++ b/examples/custom-pass/internal/di/autotag_pass.go @@ -15,6 +15,10 @@ func (p *AutoTagPass) Name() string { return "auto-tag" } +func (p *AutoTagPass) RunByDefault() bool { + return true +} + func (p *AutoTagPass) Process(cfg *di.Config) (*di.Config, error) { for id, svc := range cfg.Services { if strings.HasPrefix(id, "stdlib.") { diff --git a/examples/custom-pass/internal/di/slog_pass.go b/examples/custom-pass/internal/di/slog_pass.go index 56d434f..097d790 100644 --- a/examples/custom-pass/internal/di/slog_pass.go +++ b/examples/custom-pass/internal/di/slog_pass.go @@ -13,6 +13,10 @@ func (s *SLogPass) Name() string { return "slog" } +func (s *SLogPass) RunByDefault() bool { + return true +} + func (s *SLogPass) getTagAttributes(svc *di.Service) (map[string]any, bool) { for _, tag := range svc.Tags { if !strings.EqualFold(tag.Name, s.Name()) { diff --git a/examples/custom-pass/tools/gendi/main.go b/examples/custom-pass/tools/gendi/main.go index 18c7c85..47d852b 100644 --- a/examples/custom-pass/tools/gendi/main.go +++ b/examples/custom-pass/tools/gendi/main.go @@ -10,7 +10,7 @@ import ( func main() { // Register custom compiler passes - passes := []gendi.Pass{ + passes := []gendi.OptionalPass{ &di.AutoTagPass{}, &di.SLogPass{}, } From 6f7efaed20a3dbd512ddc1b5273f2918967d7760 Mon Sep 17 00:00:00 2001 From: asp24 <488588+asp24@users.noreply.github.com> Date: Tue, 26 May 2026 16:45:14 +0200 Subject: [PATCH 02/12] Add tests for stringSetFlag --- cmd/string_set_flag_test.go | 39 +++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 cmd/string_set_flag_test.go diff --git a/cmd/string_set_flag_test.go b/cmd/string_set_flag_test.go new file mode 100644 index 0000000..d3e9104 --- /dev/null +++ b/cmd/string_set_flag_test.go @@ -0,0 +1,39 @@ +package cmd + +import "testing" + +func TestStringSetFlag_String(t *testing.T) { + cases := []struct { + name string + m map[string]struct{} + want string + }{ + {"nil map", nil, ""}, + {"multiple values sorted", map[string]struct{}{"beta": {}, "alpha": {}, "gamma": {}}, "alpha,beta,gamma"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + f := &stringSetFlag{values: &tc.m} + if got := f.String(); got != tc.want { + t.Errorf("got %q, want %q", got, tc.want) + } + }) + } +} + +func TestStringSetFlag_Set(t *testing.T) { + var m map[string]struct{} + f := &stringSetFlag{values: &m} + f.Set("a") + f.Set("b") + f.Set("a") // duplicate + want := map[string]struct{}{"a": {}, "b": {}} + if got := len(*f.values); got != len(want) { + t.Fatalf("len = %d, want %d", got, len(want)) + } + for k := range want { + if _, ok := (*f.values)[k]; !ok { + t.Errorf("missing key %q", k) + } + } +} From ff6ffa84dbaba5db817897b7c38f177d8b4abed7 Mon Sep 17 00:00:00 2001 From: asp24 <488588+asp24@users.noreply.github.com> Date: Tue, 26 May 2026 16:54:55 +0200 Subject: [PATCH 03/12] Return error from resolvePasses on unknown or conflicting pass names --- cmd/cli.go | 7 ++- cmd/pass_config.go | 38 +++++++++++++- cmd/pass_config_test.go | 110 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 cmd/pass_config_test.go diff --git a/cmd/cli.go b/cmd/cli.go index f24d5df..4caadc9 100644 --- a/cmd/cli.go +++ b/cmd/cli.go @@ -65,7 +65,12 @@ func Run(flags *flag.FlagSet, passes []di.OptionalPass) error { return fmt.Errorf("parse flags: %w", err) } - return Generate(cfg, cfg.Passes.resolvePasses(passes)) + resolved, err := cfg.Passes.resolvePasses(passes) + if err != nil { + return fmt.Errorf("resolve passes: %w", err) + } + + return Generate(cfg, resolved) } func PrintErrorAndExit(err error) { diff --git a/cmd/pass_config.go b/cmd/pass_config.go index 7c93bb5..8c62d92 100644 --- a/cmd/pass_config.go +++ b/cmd/pass_config.go @@ -2,6 +2,7 @@ package cmd import ( "flag" + "fmt" di "github.com/asp24/gendi" ) @@ -12,9 +13,42 @@ type PassConfig struct { Disabled map[string]struct{} // Pass names to disable } +// validate Returns an error if a name appears in both Enabled and Disabled, or if any +// name in Enabled or Disabled does not match a registered pass. +func (pc *PassConfig) validate(passes []di.OptionalPass) error { + for name := range pc.Enabled { + if _, ok := pc.Disabled[name]; ok { + return fmt.Errorf("pass %q is both enabled and disabled", name) + } + } + + known := make(map[string]struct{}, len(passes)) + for _, p := range passes { + known[p.Name()] = struct{}{} + } + + for name := range pc.Enabled { + if _, ok := known[name]; !ok { + return fmt.Errorf("--enable-pass: unknown pass %q", name) + } + } + + for name := range pc.Disabled { + if _, ok := known[name]; !ok { + return fmt.Errorf("--disable-pass: unknown pass %q", name) + } + } + + return nil +} + // resolvePasses builds the final list of passes from the passed-in list, // applying enable/disable filtering and always including enabled-by-default passes. -func (pc *PassConfig) resolvePasses(passes []di.OptionalPass) []di.Pass { +func (pc *PassConfig) resolvePasses(passes []di.OptionalPass) ([]di.Pass, error) { + if err := pc.validate(passes); err != nil { + return nil, err + } + result := make([]di.Pass, 0, len(passes)) included := make(map[string]struct{}, len(passes)) for _, p := range passes { @@ -36,7 +70,7 @@ func (pc *PassConfig) resolvePasses(passes []di.OptionalPass) []di.Pass { included[name] = struct{}{} } - return result + return result, nil } // RegisterFlags adds pass enable/disable flags to the flag set. diff --git a/cmd/pass_config_test.go b/cmd/pass_config_test.go new file mode 100644 index 0000000..73b742a --- /dev/null +++ b/cmd/pass_config_test.go @@ -0,0 +1,110 @@ +package cmd + +import ( + "testing" + + di "github.com/asp24/gendi" +) + +type testPass struct { + name string + runByDefault bool +} + +func (p *testPass) Name() string { return p.name } +func (p *testPass) RunByDefault() bool { return p.runByDefault } +func (p *testPass) Process(cfg *di.Config) (*di.Config, error) { return cfg, nil } + +func makePass(name string, runByDefault bool) *testPass { + return &testPass{name: name, runByDefault: runByDefault} +} + +func TestPassConfig_ResolvePasses(t *testing.T) { + cases := []struct { + name string + enabled map[string]struct{} + disabled map[string]struct{} + passes []di.OptionalPass + wantNames []string + }{ + { + name: "default-on pass is included", + passes: []di.OptionalPass{makePass("a", true)}, + wantNames: []string{"a"}, + }, + { + name: "default-off pass is excluded", + passes: []di.OptionalPass{makePass("a", false)}, + wantNames: []string{}, + }, + { + name: "default-off pass included when enabled", + enabled: map[string]struct{}{"a": {}}, + passes: []di.OptionalPass{makePass("a", false)}, + wantNames: []string{"a"}, + }, + { + name: "default-on pass excluded when disabled", + disabled: map[string]struct{}{"a": {}}, + passes: []di.OptionalPass{makePass("a", true)}, + wantNames: []string{}, + }, + { + name: "duplicate pass name runs only once", + passes: []di.OptionalPass{makePass("a", true), makePass("a", true)}, + wantNames: []string{"a"}, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + pc := PassConfig{Enabled: tc.enabled, Disabled: tc.disabled} + result, err := pc.resolvePasses(tc.passes) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(result) != len(tc.wantNames) { + t.Fatalf("got %d passes, want %d", len(result), len(tc.wantNames)) + } + for i, p := range result { + if p.Name() != tc.wantNames[i] { + t.Errorf("pass[%d] name = %q, want %q", i, p.Name(), tc.wantNames[i]) + } + } + }) + } +} + +func TestPassConfig_ResolvePasses_Errors(t *testing.T) { + cases := []struct { + name string + enabled map[string]struct{} + disabled map[string]struct{} + passes []di.OptionalPass + }{ + { + name: "unknown name in --enable-pass", + enabled: map[string]struct{}{"unknown": {}}, + passes: []di.OptionalPass{makePass("foo", true)}, + }, + { + name: "unknown name in --disable-pass", + disabled: map[string]struct{}{"unknown": {}}, + passes: []di.OptionalPass{makePass("foo", true)}, + }, + { + name: "same name in both --enable-pass and --disable-pass", + enabled: map[string]struct{}{"foo": {}}, + disabled: map[string]struct{}{"foo": {}}, + passes: []di.OptionalPass{makePass("foo", true)}, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + pc := PassConfig{Enabled: tc.enabled, Disabled: tc.disabled} + _, err := pc.resolvePasses(tc.passes) + if err == nil { + t.Error("expected error, got nil") + } + }) + } +} From 36129613378ee6986cd956f5861c4cd060c9c89b Mon Sep 17 00:00:00 2001 From: asp24 <488588+asp24@users.noreply.github.com> Date: Tue, 26 May 2026 17:23:05 +0200 Subject: [PATCH 04/12] Update docs for SLogPass move to stdlib and pass flag validation --- doc/custom-passes.md | 4 +++ doc/spec/cli.md | 4 +-- stdlib/README.md | 29 +++++++++++++++++++ .../internal/di => stdlib}/slog_pass.go | 0 4 files changed, 35 insertions(+), 2 deletions(-) rename {examples/custom-pass/internal/di => stdlib}/slog_pass.go (100%) diff --git a/doc/custom-passes.md b/doc/custom-passes.md index 4ef8fd3..a921a96 100644 --- a/doc/custom-passes.md +++ b/doc/custom-passes.md @@ -87,6 +87,10 @@ type OptionalPass interface { - Pass names come from `Name()`. - If the same pass name is registered more than once, only the first included pass runs. +`cmd.Run` validates pass flags before generation and returns an error if: +- A name passed to `--enable-pass` or `--disable-pass` does not match any registered pass. +- The same name appears in both `--enable-pass` and `--disable-pass`. + Use `di.Pass` when calling `di.ApplyPasses` or `cmd.Generate` directly. Use `di.OptionalPass` when registering passes with `cmd.Run` or `cmd.MustRun`. ## Creating a Pass diff --git a/doc/spec/cli.md b/doc/spec/cli.md index 8f831cd..f5e2708 100644 --- a/doc/spec/cli.md +++ b/doc/spec/cli.md @@ -16,8 +16,8 @@ gendi | `--container` | Container struct name | | `--strict` | Enable strict validation (default: true) | | `--build-tags` | Go build tags | -| `--enable-pass` | Enable an optional compiler pass by name; repeat for multiple passes | -| `--disable-pass` | Disable an optional compiler pass by name; repeat for multiple passes | +| `--enable-pass` | Enable an optional compiler pass by name; repeat for multiple passes; errors on unknown name or conflict with `--disable-pass` | +| `--disable-pass` | Disable an optional compiler pass by name; repeat for multiple passes; errors on unknown name or conflict with `--enable-pass` | | `--verbose` | Verbose logging | ## go:generate diff --git a/stdlib/README.md b/stdlib/README.md index 5c40afa..6f16fc5 100644 --- a/stdlib/README.md +++ b/stdlib/README.md @@ -549,6 +549,35 @@ services: shared: true ``` +## Compiler Passes + +The stdlib package also provides optional compiler passes for use in custom generator binaries. + +### SLogPass + +**Pass name:** `slog` + +Automatically wires structured logging into services that follow the slog naming convention. Use it in a custom generator built with `cmd.Run` or `cmd.MustRun`: + +```go +import ( + "flag" + "github.com/asp24/gendi/cmd" + "github.com/asp24/gendi/stdlib" +) + +func main() { + passes := []gendi.OptionalPass{ + stdlib.NewSLogPass(true), // true = run by default + } + cmd.MustRun(flag.CommandLine, passes) +} +``` + +`NewSLogPass(runByDefault bool)` controls whether the pass runs unless explicitly toggled via CLI: +- `true` — runs unless the user passes `--disable-pass=slog` +- `false` — skipped unless the user passes `--enable-pass=slog` + ## See Also - [Configuration Reference](../doc/configuration.md) diff --git a/examples/custom-pass/internal/di/slog_pass.go b/stdlib/slog_pass.go similarity index 100% rename from examples/custom-pass/internal/di/slog_pass.go rename to stdlib/slog_pass.go From ef6938052be06d5fd5e8bad589ff06de006fef9d Mon Sep 17 00:00:00 2001 From: asp24 <488588+asp24@users.noreply.github.com> Date: Tue, 26 May 2026 17:24:02 +0200 Subject: [PATCH 05/12] Reorder pass validation logic in `PassConfig.validate` for clarity. --- cmd/pass_config.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/pass_config.go b/cmd/pass_config.go index 8c62d92..b5a42ed 100644 --- a/cmd/pass_config.go +++ b/cmd/pass_config.go @@ -16,12 +16,6 @@ type PassConfig struct { // validate Returns an error if a name appears in both Enabled and Disabled, or if any // name in Enabled or Disabled does not match a registered pass. func (pc *PassConfig) validate(passes []di.OptionalPass) error { - for name := range pc.Enabled { - if _, ok := pc.Disabled[name]; ok { - return fmt.Errorf("pass %q is both enabled and disabled", name) - } - } - known := make(map[string]struct{}, len(passes)) for _, p := range passes { known[p.Name()] = struct{}{} @@ -39,6 +33,12 @@ func (pc *PassConfig) validate(passes []di.OptionalPass) error { } } + for name := range pc.Enabled { + if _, ok := pc.Disabled[name]; ok { + return fmt.Errorf("pass %q is both enabled and disabled", name) + } + } + return nil } From a06cfda033c046728a129832ce3d418c061f6d0a Mon Sep 17 00:00:00 2001 From: asp24 <488588+asp24@users.noreply.github.com> Date: Tue, 26 May 2026 17:25:40 +0200 Subject: [PATCH 06/12] Move `SLogPass` to `stdlib`, make `runByDefault` configurable, and update custom-pass example --- examples/custom-pass/tools/gendi/main.go | 3 ++- stdlib/slog_pass.go | 9 +++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/examples/custom-pass/tools/gendi/main.go b/examples/custom-pass/tools/gendi/main.go index 47d852b..07ed606 100644 --- a/examples/custom-pass/tools/gendi/main.go +++ b/examples/custom-pass/tools/gendi/main.go @@ -6,13 +6,14 @@ import ( gendi "github.com/asp24/gendi" "github.com/asp24/gendi/cmd" "github.com/asp24/gendi/examples/custom-pass/internal/di" + "github.com/asp24/gendi/stdlib" ) func main() { // Register custom compiler passes passes := []gendi.OptionalPass{ &di.AutoTagPass{}, - &di.SLogPass{}, + stdlib.NewSLogPass(true), } cmd.MustRun(flag.CommandLine, passes) diff --git a/stdlib/slog_pass.go b/stdlib/slog_pass.go index 097d790..323aa15 100644 --- a/stdlib/slog_pass.go +++ b/stdlib/slog_pass.go @@ -1,4 +1,4 @@ -package di +package stdlib import ( "strings" @@ -7,6 +7,11 @@ import ( ) type SLogPass struct { + runByDefault bool +} + +func NewSLogPass(runByDefault bool) *SLogPass { + return &SLogPass{runByDefault: runByDefault} } func (s *SLogPass) Name() string { @@ -14,7 +19,7 @@ func (s *SLogPass) Name() string { } func (s *SLogPass) RunByDefault() bool { - return true + return s.runByDefault } func (s *SLogPass) getTagAttributes(svc *di.Service) (map[string]any, bool) { From c015f8e3c4841e112c630b712dc8539809953af0 Mon Sep 17 00:00:00 2001 From: asp24 <488588+asp24@users.noreply.github.com> Date: Tue, 26 May 2026 17:28:04 +0200 Subject: [PATCH 07/12] Fix integration tests failing due to VCS stamping in temp build dirs --- integration/integration_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/integration_test.go b/integration/integration_test.go index 7902194..6568dbf 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -63,7 +63,7 @@ func runEmbeddedTest(t *testing.T, testName string, expectedOutput string, wantC } // Compile the code - compileCmd := exec.Command("go", "build", "-o", "app") + compileCmd := exec.Command("go", "build", "-buildvcs=false", "-o", "app") compileCmd.Dir = tmpDir compileOutput, err := compileCmd.CombinedOutput() if err != nil { From 594ca426455fccf3ac111ab6f7d42e89d97d879d Mon Sep 17 00:00:00 2001 From: asp24 <488588+asp24@users.noreply.github.com> Date: Tue, 26 May 2026 20:35:56 +0200 Subject: [PATCH 08/12] Add ExposeAllPass, rename OptionalPass to SelectablePass, introduce BuiltinSelectablePasses --- AGENTS.md | 2 +- cmd/cli.go | 18 ++++++++++---- cmd/gendi/main.go | 2 +- cmd/pass_config.go | 4 +-- cmd/pass_config_test.go | 20 +++++++-------- config.go | 5 +++- doc/custom-passes.md | 8 +++--- examples/custom-pass/tools/gendi/main.go | 2 +- public_pass.go | 31 ++++++++++++++++++++++++ 9 files changed, 67 insertions(+), 25 deletions(-) create mode 100644 public_pass.go diff --git a/AGENTS.md b/AGENTS.md index c2908b6..6357a4c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -242,7 +242,7 @@ Use in custom generator: ```go // tools/gendi/main.go func main() { - passes := []di.OptionalPass{&MyPass{}} + passes := []di.SelectablePass{&MyPass{}} cmd.Run(flag.CommandLine, passes) } ``` diff --git a/cmd/cli.go b/cmd/cli.go index 4caadc9..5905bf1 100644 --- a/cmd/cli.go +++ b/cmd/cli.go @@ -9,9 +9,17 @@ import ( di "github.com/asp24/gendi" "github.com/asp24/gendi/pipeline" "github.com/asp24/gendi/srcloc" + "github.com/asp24/gendi/stdlib" "github.com/asp24/gendi/yaml" ) +func BuiltinSelectablePasses() []di.SelectablePass { + return []di.SelectablePass{ + stdlib.NewSLogPass(false), + &di.ExposeAllPass{}, + } +} + // WriteTargetFile writes data to the specified file path func WriteTargetFile(path string, data []byte) error { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { @@ -57,7 +65,7 @@ func Generate(cfg Config, passes []di.Pass) error { } // Run executes the full gendi workflow with optional compiler passes -func Run(flags *flag.FlagSet, passes []di.OptionalPass) error { +func Run(flags *flag.FlagSet, selectablePasses []di.SelectablePass) error { var cfg Config cfg.RegisterFlags(flags) @@ -65,12 +73,12 @@ func Run(flags *flag.FlagSet, passes []di.OptionalPass) error { return fmt.Errorf("parse flags: %w", err) } - resolved, err := cfg.Passes.resolvePasses(passes) + passes, err := cfg.Passes.resolvePasses(selectablePasses) if err != nil { return fmt.Errorf("resolve passes: %w", err) } - return Generate(cfg, resolved) + return Generate(cfg, passes) } func PrintErrorAndExit(err error) { @@ -84,6 +92,6 @@ func PrintErrorAndExit(err error) { os.Exit(1) } -func MustRun(flags *flag.FlagSet, passes []di.OptionalPass) { - PrintErrorAndExit(Run(flags, passes)) +func MustRun(flags *flag.FlagSet, selectablePasses []di.SelectablePass) { + PrintErrorAndExit(Run(flags, selectablePasses)) } diff --git a/cmd/gendi/main.go b/cmd/gendi/main.go index c5ffd36..c3cf77f 100644 --- a/cmd/gendi/main.go +++ b/cmd/gendi/main.go @@ -7,5 +7,5 @@ import ( ) func main() { - cmd.MustRun(flag.CommandLine, nil) + cmd.MustRun(flag.CommandLine, cmd.BuiltinSelectablePasses()) } diff --git a/cmd/pass_config.go b/cmd/pass_config.go index b5a42ed..d419fff 100644 --- a/cmd/pass_config.go +++ b/cmd/pass_config.go @@ -15,7 +15,7 @@ type PassConfig struct { // validate Returns an error if a name appears in both Enabled and Disabled, or if any // name in Enabled or Disabled does not match a registered pass. -func (pc *PassConfig) validate(passes []di.OptionalPass) error { +func (pc *PassConfig) validate(passes []di.SelectablePass) error { known := make(map[string]struct{}, len(passes)) for _, p := range passes { known[p.Name()] = struct{}{} @@ -44,7 +44,7 @@ func (pc *PassConfig) validate(passes []di.OptionalPass) error { // resolvePasses builds the final list of passes from the passed-in list, // applying enable/disable filtering and always including enabled-by-default passes. -func (pc *PassConfig) resolvePasses(passes []di.OptionalPass) ([]di.Pass, error) { +func (pc *PassConfig) resolvePasses(passes []di.SelectablePass) ([]di.Pass, error) { if err := pc.validate(passes); err != nil { return nil, err } diff --git a/cmd/pass_config_test.go b/cmd/pass_config_test.go index 73b742a..eb42a80 100644 --- a/cmd/pass_config_test.go +++ b/cmd/pass_config_test.go @@ -24,34 +24,34 @@ func TestPassConfig_ResolvePasses(t *testing.T) { name string enabled map[string]struct{} disabled map[string]struct{} - passes []di.OptionalPass + passes []di.SelectablePass wantNames []string }{ { name: "default-on pass is included", - passes: []di.OptionalPass{makePass("a", true)}, + passes: []di.SelectablePass{makePass("a", true)}, wantNames: []string{"a"}, }, { name: "default-off pass is excluded", - passes: []di.OptionalPass{makePass("a", false)}, + passes: []di.SelectablePass{makePass("a", false)}, wantNames: []string{}, }, { name: "default-off pass included when enabled", enabled: map[string]struct{}{"a": {}}, - passes: []di.OptionalPass{makePass("a", false)}, + passes: []di.SelectablePass{makePass("a", false)}, wantNames: []string{"a"}, }, { name: "default-on pass excluded when disabled", disabled: map[string]struct{}{"a": {}}, - passes: []di.OptionalPass{makePass("a", true)}, + passes: []di.SelectablePass{makePass("a", true)}, wantNames: []string{}, }, { name: "duplicate pass name runs only once", - passes: []di.OptionalPass{makePass("a", true), makePass("a", true)}, + passes: []di.SelectablePass{makePass("a", true), makePass("a", true)}, wantNames: []string{"a"}, }, } @@ -79,23 +79,23 @@ func TestPassConfig_ResolvePasses_Errors(t *testing.T) { name string enabled map[string]struct{} disabled map[string]struct{} - passes []di.OptionalPass + passes []di.SelectablePass }{ { name: "unknown name in --enable-pass", enabled: map[string]struct{}{"unknown": {}}, - passes: []di.OptionalPass{makePass("foo", true)}, + passes: []di.SelectablePass{makePass("foo", true)}, }, { name: "unknown name in --disable-pass", disabled: map[string]struct{}{"unknown": {}}, - passes: []di.OptionalPass{makePass("foo", true)}, + passes: []di.SelectablePass{makePass("foo", true)}, }, { name: "same name in both --enable-pass and --disable-pass", enabled: map[string]struct{}{"foo": {}}, disabled: map[string]struct{}{"foo": {}}, - passes: []di.OptionalPass{makePass("foo", true)}, + passes: []di.SelectablePass{makePass("foo", true)}, }, } for _, tc := range cases { diff --git a/config.go b/config.go index 92130f0..860edab 100644 --- a/config.go +++ b/config.go @@ -13,7 +13,10 @@ type Pass interface { Process(cfg *Config) (*Config, error) } -type OptionalPass interface { +// SelectablePass extends Pass with a default execution policy that CLI flags +// can override: RunByDefault()=true runs unless --disable-pass= is set; +// RunByDefault()=false skips unless --enable-pass= is set. +type SelectablePass interface { Pass RunByDefault() bool } diff --git a/doc/custom-passes.md b/doc/custom-passes.md index a921a96..611d764 100644 --- a/doc/custom-passes.md +++ b/doc/custom-passes.md @@ -74,7 +74,7 @@ Custom generator binaries built with `cmd.Run` register optional passes. Optiona ```go package di -type OptionalPass interface { +type SelectablePass interface { Pass RunByDefault() bool } @@ -91,7 +91,7 @@ type OptionalPass interface { - A name passed to `--enable-pass` or `--disable-pass` does not match any registered pass. - The same name appears in both `--enable-pass` and `--disable-pass`. -Use `di.Pass` when calling `di.ApplyPasses` or `cmd.Generate` directly. Use `di.OptionalPass` when registering passes with `cmd.Run` or `cmd.MustRun`. +Use `di.Pass` when calling `di.ApplyPasses` or `cmd.Generate` directly. Use `di.SelectablePass` when registering passes with `cmd.Run` or `cmd.MustRun`. ## Creating a Pass @@ -222,7 +222,7 @@ import ( func main() { // Define custom compiler passes - customPasses := []di.OptionalPass{ + customPasses := []di.SelectablePass{ &passes.AutoTagPass{}, &passes.ValidationPass{}, } @@ -528,7 +528,7 @@ customPasses := []di.Pass{ } ``` -If these passes are registered with `cmd.Run`, use `[]di.OptionalPass` and implement `RunByDefault` on each pass. +If these passes are registered with `cmd.Run`, use `[]di.SelectablePass` and implement `RunByDefault` on each pass. ## Complete Example diff --git a/examples/custom-pass/tools/gendi/main.go b/examples/custom-pass/tools/gendi/main.go index 07ed606..9acdd1e 100644 --- a/examples/custom-pass/tools/gendi/main.go +++ b/examples/custom-pass/tools/gendi/main.go @@ -11,7 +11,7 @@ import ( func main() { // Register custom compiler passes - passes := []gendi.OptionalPass{ + passes := []gendi.SelectablePass{ &di.AutoTagPass{}, stdlib.NewSLogPass(true), } diff --git a/public_pass.go b/public_pass.go new file mode 100644 index 0000000..255b5ed --- /dev/null +++ b/public_pass.go @@ -0,0 +1,31 @@ +package di + +// ExposeAllPass is a test-only pass that promotes every service to public, +// causing the generator to emit a public getter for each one. Use it when +// building a test DI container that needs direct access to all services +// regardless of how they are declared in the YAML config. +// +// Enable via: --enable-pass=expose-all +// +// Not intended for production containers — it overrides explicit `public: false` +// declarations and disables unreachable-service pruning (all services become +// reachable roots), so every imported service gets a generated getter. +type ExposeAllPass struct { +} + +func (p *ExposeAllPass) Name() string { + return "expose-all" +} + +func (p *ExposeAllPass) RunByDefault() bool { + return false +} + +func (p *ExposeAllPass) Process(cfg *Config) (*Config, error) { + for id, svc := range cfg.Services { + svc.Public = true + cfg.Services[id] = svc + } + + return cfg, nil +} From d20cb1c88bc203263a820afb0c71ffb0fd519fac Mon Sep 17 00:00:00 2001 From: asp24 <488588+asp24@users.noreply.github.com> Date: Wed, 27 May 2026 09:02:26 +0200 Subject: [PATCH 09/12] Remove --disable-pass CLI option support --- README.md | 2 -- cmd/pass_config.go | 29 +++++------------------------ cmd/pass_config_test.go | 29 +++++------------------------ config.go | 2 +- doc/LLM.md | 2 +- doc/custom-passes.md | 9 ++------- doc/spec/cli.md | 3 +-- examples/custom-pass/README.md | 3 ++- stdlib/README.md | 2 +- 9 files changed, 18 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index 2df3644..3e113cd 100644 --- a/README.md +++ b/README.md @@ -136,8 +136,6 @@ Flags: --build-tags string Build tags for generated file --enable-pass string Enable an optional compiler pass (repeatable) - --disable-pass string - Disable an optional compiler pass (repeatable) --verbose Enable verbose logging ``` diff --git a/cmd/pass_config.go b/cmd/pass_config.go index d419fff..7341b0e 100644 --- a/cmd/pass_config.go +++ b/cmd/pass_config.go @@ -7,14 +7,12 @@ import ( di "github.com/asp24/gendi" ) -// PassConfig holds enabled/disabled pass configuration. +// PassConfig holds enabled pass configuration. type PassConfig struct { - Enabled map[string]struct{} // Pass names to enable - Disabled map[string]struct{} // Pass names to disable + Enabled map[string]struct{} // Pass names to enable } -// validate Returns an error if a name appears in both Enabled and Disabled, or if any -// name in Enabled or Disabled does not match a registered pass. +// validate Returns an error if any name in Enabled does not match a registered pass. func (pc *PassConfig) validate(passes []di.SelectablePass) error { known := make(map[string]struct{}, len(passes)) for _, p := range passes { @@ -27,23 +25,11 @@ func (pc *PassConfig) validate(passes []di.SelectablePass) error { } } - for name := range pc.Disabled { - if _, ok := known[name]; !ok { - return fmt.Errorf("--disable-pass: unknown pass %q", name) - } - } - - for name := range pc.Enabled { - if _, ok := pc.Disabled[name]; ok { - return fmt.Errorf("pass %q is both enabled and disabled", name) - } - } - return nil } // resolvePasses builds the final list of passes from the passed-in list, -// applying enable/disable filtering and always including enabled-by-default passes. +// applying enable filtering and always including enabled-by-default passes. func (pc *PassConfig) resolvePasses(passes []di.SelectablePass) ([]di.Pass, error) { if err := pc.validate(passes); err != nil { return nil, err @@ -62,10 +48,6 @@ func (pc *PassConfig) resolvePasses(passes []di.SelectablePass) ([]di.Pass, erro continue } - if _, disabled := pc.Disabled[name]; disabled { - continue - } - result = append(result, p) included[name] = struct{}{} } @@ -73,8 +55,7 @@ func (pc *PassConfig) resolvePasses(passes []di.SelectablePass) ([]di.Pass, erro return result, nil } -// RegisterFlags adds pass enable/disable flags to the flag set. +// RegisterFlags adds pass enable flags to the flag set. func (pc *PassConfig) RegisterFlags(flags *flag.FlagSet) { flags.Var(&stringSetFlag{values: &pc.Enabled}, "enable-pass", "Enable a specific compiler pass (can be specified multiple times)") - flags.Var(&stringSetFlag{values: &pc.Disabled}, "disable-pass", "Disable a specific compiler pass (can be specified multiple times)") } diff --git a/cmd/pass_config_test.go b/cmd/pass_config_test.go index eb42a80..2b05d53 100644 --- a/cmd/pass_config_test.go +++ b/cmd/pass_config_test.go @@ -23,7 +23,6 @@ func TestPassConfig_ResolvePasses(t *testing.T) { cases := []struct { name string enabled map[string]struct{} - disabled map[string]struct{} passes []di.SelectablePass wantNames []string }{ @@ -43,12 +42,6 @@ func TestPassConfig_ResolvePasses(t *testing.T) { passes: []di.SelectablePass{makePass("a", false)}, wantNames: []string{"a"}, }, - { - name: "default-on pass excluded when disabled", - disabled: map[string]struct{}{"a": {}}, - passes: []di.SelectablePass{makePass("a", true)}, - wantNames: []string{}, - }, { name: "duplicate pass name runs only once", passes: []di.SelectablePass{makePass("a", true), makePass("a", true)}, @@ -57,7 +50,7 @@ func TestPassConfig_ResolvePasses(t *testing.T) { } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - pc := PassConfig{Enabled: tc.enabled, Disabled: tc.disabled} + pc := PassConfig{Enabled: tc.enabled} result, err := pc.resolvePasses(tc.passes) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -76,31 +69,19 @@ func TestPassConfig_ResolvePasses(t *testing.T) { func TestPassConfig_ResolvePasses_Errors(t *testing.T) { cases := []struct { - name string - enabled map[string]struct{} - disabled map[string]struct{} - passes []di.SelectablePass + name string + enabled map[string]struct{} + passes []di.SelectablePass }{ { name: "unknown name in --enable-pass", enabled: map[string]struct{}{"unknown": {}}, passes: []di.SelectablePass{makePass("foo", true)}, }, - { - name: "unknown name in --disable-pass", - disabled: map[string]struct{}{"unknown": {}}, - passes: []di.SelectablePass{makePass("foo", true)}, - }, - { - name: "same name in both --enable-pass and --disable-pass", - enabled: map[string]struct{}{"foo": {}}, - disabled: map[string]struct{}{"foo": {}}, - passes: []di.SelectablePass{makePass("foo", true)}, - }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - pc := PassConfig{Enabled: tc.enabled, Disabled: tc.disabled} + pc := PassConfig{Enabled: tc.enabled} _, err := pc.resolvePasses(tc.passes) if err == nil { t.Error("expected error, got nil") diff --git a/config.go b/config.go index 860edab..8a0e4f6 100644 --- a/config.go +++ b/config.go @@ -14,7 +14,7 @@ type Pass interface { } // SelectablePass extends Pass with a default execution policy that CLI flags -// can override: RunByDefault()=true runs unless --disable-pass= is set; +// can override: RunByDefault()=true runs by default; // RunByDefault()=false skips unless --enable-pass= is set. type SelectablePass interface { Pass diff --git a/doc/LLM.md b/doc/LLM.md index c292013..a1b5453 100644 --- a/doc/LLM.md +++ b/doc/LLM.md @@ -8,7 +8,7 @@ Short, stable facts for tooling and assistants. go tool gendi --config=gendi.yaml --out=./di --pkg=di ``` -Flags: `--config`, `--out`, `--pkg`, `--container`, `--strict`, `--build-tags`, `--enable-pass`, `--disable-pass`, `--verbose`. +Flags: `--config`, `--out`, `--pkg`, `--container`, `--strict`, `--build-tags`, `--enable-pass`, `--verbose`. ## YAML Syntax diff --git a/doc/custom-passes.md b/doc/custom-passes.md index 611d764..c2ab1bc 100644 --- a/doc/custom-passes.md +++ b/doc/custom-passes.md @@ -82,14 +82,12 @@ type SelectablePass interface { `RunByDefault` controls how the pass participates in CLI filtering: -- Return `true` to run the pass unless the user passes `--disable-pass=`. +- Return `true` to run the pass by default. - Return `false` to skip the pass unless the user passes `--enable-pass=`. - Pass names come from `Name()`. - If the same pass name is registered more than once, only the first included pass runs. -`cmd.Run` validates pass flags before generation and returns an error if: -- A name passed to `--enable-pass` or `--disable-pass` does not match any registered pass. -- The same name appears in both `--enable-pass` and `--disable-pass`. +`cmd.Run` validates pass flags before generation and returns an error if a name passed to `--enable-pass` does not match any registered pass. Use `di.Pass` when calling `di.ApplyPasses` or `cmd.Generate` directly. Use `di.SelectablePass` when registering passes with `cmd.Run` or `cmd.MustRun`. @@ -244,9 +242,6 @@ go build -o bin/gendi ./tools/gendi # Run custom generator ./bin/gendi --config=gendi.yaml --out=./di --pkg=di -# Disable a default-enabled optional pass -./bin/gendi --config=gendi.yaml --out=./di --pkg=di --disable-pass=auto-tag - # Enable a default-disabled optional pass ./bin/gendi --config=gendi.yaml --out=./di --pkg=di --enable-pass=validation diff --git a/doc/spec/cli.md b/doc/spec/cli.md index f5e2708..c10f05a 100644 --- a/doc/spec/cli.md +++ b/doc/spec/cli.md @@ -16,8 +16,7 @@ gendi | `--container` | Container struct name | | `--strict` | Enable strict validation (default: true) | | `--build-tags` | Go build tags | -| `--enable-pass` | Enable an optional compiler pass by name; repeat for multiple passes; errors on unknown name or conflict with `--disable-pass` | -| `--disable-pass` | Disable an optional compiler pass by name; repeat for multiple passes; errors on unknown name or conflict with `--enable-pass` | +| `--enable-pass` | Enable an optional compiler pass by name; repeat for multiple passes; errors on unknown name | | `--verbose` | Verbose logging | ## go:generate diff --git a/examples/custom-pass/README.md b/examples/custom-pass/README.md index c0201bc..080d00e 100644 --- a/examples/custom-pass/README.md +++ b/examples/custom-pass/README.md @@ -136,7 +136,8 @@ The `cmd/main.go` includes a go:generate directive: go run ./tools/gendi --config=./cmd/gendi.yaml --out=./cmd --pkg=main # Disable a default-enabled optional pass -go run ./tools/gendi --config=./cmd/gendi.yaml --out=./cmd --pkg=main --disable-pass=slog +# To disable the SLog pass: +go run ./tools/gendi --config=./cmd/gendi.yaml --out=./cmd --pkg=main # Run the demo go run ./cmd/*.go diff --git a/stdlib/README.md b/stdlib/README.md index 6f16fc5..7f508c8 100644 --- a/stdlib/README.md +++ b/stdlib/README.md @@ -575,7 +575,7 @@ func main() { ``` `NewSLogPass(runByDefault bool)` controls whether the pass runs unless explicitly toggled via CLI: -- `true` — runs unless the user passes `--disable-pass=slog` +- `true` — runs by default - `false` — skipped unless the user passes `--enable-pass=slog` ## See Also From 7c092e48f0d5b6d349df578ea8dd171cb5c1afb0 Mon Sep 17 00:00:00 2001 From: asp24 <488588+asp24@users.noreply.github.com> Date: Wed, 27 May 2026 10:52:02 +0200 Subject: [PATCH 10/12] Separate and deduplicate compiler passes --- AGENTS.md | 9 +- README.md | 2 - cmd/cli.go | 18 ++-- cmd/gendi/main.go | 2 +- cmd/pass_config.go | 32 ++++--- cmd/pass_config_test.go | 83 ++++++++++++------- config.go | 8 -- doc/custom-passes.md | 45 ++++------ doc/spec/cli.md | 2 +- examples/custom-pass/README.md | 26 ++---- .../custom-pass/internal/di/autotag_pass.go | 4 - examples/custom-pass/tools/gendi/main.go | 6 +- public_pass.go | 4 - stdlib/README.md | 24 ++++-- stdlib/slog_pass.go | 12 +-- 15 files changed, 138 insertions(+), 139 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6357a4c..403ea63 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -230,8 +230,6 @@ type MyPass struct{} func (p *MyPass) Name() string { return "my-pass" } -func (p *MyPass) RunByDefault() bool { return true } - func (p *MyPass) Process(cfg *di.Config) (*di.Config, error) { // Mutate cfg (add services, modify args, etc.) return cfg, nil @@ -242,8 +240,11 @@ Use in custom generator: ```go // tools/gendi/main.go func main() { - passes := []di.SelectablePass{&MyPass{}} - cmd.Run(flag.CommandLine, passes) + // Always-included passes + passes := []di.Pass{&MyPass{}} + // Selectable passes (filtered by --enable-pass flag) + selectablePasses := []di.Pass{} + cmd.Run(flag.CommandLine, passes, selectablePasses) } ``` diff --git a/README.md b/README.md index 3e113cd..5cc58d2 100644 --- a/README.md +++ b/README.md @@ -161,8 +161,6 @@ type AutoTagPass struct{} func (p *AutoTagPass) Name() string { return "auto-tag" } -func (p *AutoTagPass) RunByDefault() bool { return true } - func (p *AutoTagPass) Process(cfg *di.Config) (*di.Config, error) { for id, svc := range cfg.Services { if strings.HasSuffix(id, ".handler") { diff --git a/cmd/cli.go b/cmd/cli.go index 5905bf1..bf027b6 100644 --- a/cmd/cli.go +++ b/cmd/cli.go @@ -13,9 +13,9 @@ import ( "github.com/asp24/gendi/yaml" ) -func BuiltinSelectablePasses() []di.SelectablePass { - return []di.SelectablePass{ - stdlib.NewSLogPass(false), +func BuiltinSelectablePasses() []di.Pass { + return []di.Pass{ + &stdlib.SLogPass{}, &di.ExposeAllPass{}, } } @@ -64,8 +64,8 @@ func Generate(cfg Config, passes []di.Pass) error { return nil } -// Run executes the full gendi workflow with optional compiler passes -func Run(flags *flag.FlagSet, selectablePasses []di.SelectablePass) error { +// Run executes the full gendi workflow with compiler passes +func Run(flags *flag.FlagSet, passes, selectablePasses []di.Pass) error { var cfg Config cfg.RegisterFlags(flags) @@ -73,12 +73,12 @@ func Run(flags *flag.FlagSet, selectablePasses []di.SelectablePass) error { return fmt.Errorf("parse flags: %w", err) } - passes, err := cfg.Passes.resolvePasses(selectablePasses) + resolvedPasses, err := cfg.Passes.resolvePasses(passes, selectablePasses) if err != nil { return fmt.Errorf("resolve passes: %w", err) } - return Generate(cfg, passes) + return Generate(cfg, resolvedPasses) } func PrintErrorAndExit(err error) { @@ -92,6 +92,6 @@ func PrintErrorAndExit(err error) { os.Exit(1) } -func MustRun(flags *flag.FlagSet, selectablePasses []di.SelectablePass) { - PrintErrorAndExit(Run(flags, selectablePasses)) +func MustRun(flags *flag.FlagSet, passes, selectablePasses []di.Pass) { + PrintErrorAndExit(Run(flags, passes, selectablePasses)) } diff --git a/cmd/gendi/main.go b/cmd/gendi/main.go index c3cf77f..6ee4317 100644 --- a/cmd/gendi/main.go +++ b/cmd/gendi/main.go @@ -7,5 +7,5 @@ import ( ) func main() { - cmd.MustRun(flag.CommandLine, cmd.BuiltinSelectablePasses()) + cmd.MustRun(flag.CommandLine, nil, cmd.BuiltinSelectablePasses()) } diff --git a/cmd/pass_config.go b/cmd/pass_config.go index 7341b0e..faf5f41 100644 --- a/cmd/pass_config.go +++ b/cmd/pass_config.go @@ -12,10 +12,10 @@ type PassConfig struct { Enabled map[string]struct{} // Pass names to enable } -// validate Returns an error if any name in Enabled does not match a registered pass. -func (pc *PassConfig) validate(passes []di.SelectablePass) error { - known := make(map[string]struct{}, len(passes)) - for _, p := range passes { +// validate Returns an error if any name in Enabled does not match a selectable pass. +func (pc *PassConfig) validate(selectablePasses []di.Pass) error { + known := make(map[string]struct{}, len(selectablePasses)) + for _, p := range selectablePasses { known[p.Name()] = struct{}{} } @@ -28,23 +28,33 @@ func (pc *PassConfig) validate(passes []di.SelectablePass) error { return nil } -// resolvePasses builds the final list of passes from the passed-in list, -// applying enable filtering and always including enabled-by-default passes. -func (pc *PassConfig) resolvePasses(passes []di.SelectablePass) ([]di.Pass, error) { - if err := pc.validate(passes); err != nil { +// resolvePasses builds the final list of passes, applying enable filtering to +// selectable passes and deduplicating by pass name. +func (pc *PassConfig) resolvePasses(passes, selectablePasses []di.Pass) ([]di.Pass, error) { + if err := pc.validate(selectablePasses); err != nil { return nil, err } - result := make([]di.Pass, 0, len(passes)) - included := make(map[string]struct{}, len(passes)) + result := make([]di.Pass, 0, len(passes)+len(selectablePasses)) + included := make(map[string]struct{}, len(passes)+len(selectablePasses)) for _, p := range passes { name := p.Name() if _, ok := included[name]; ok { continue } + result = append(result, p) + included[name] = struct{}{} + } + + for _, p := range selectablePasses { + name := p.Name() + if _, ok := included[name]; ok { + continue + } + _, enabled := pc.Enabled[name] - if !p.RunByDefault() && !enabled { + if !enabled { continue } diff --git a/cmd/pass_config_test.go b/cmd/pass_config_test.go index 2b05d53..910522c 100644 --- a/cmd/pass_config_test.go +++ b/cmd/pass_config_test.go @@ -7,51 +7,70 @@ import ( ) type testPass struct { - name string - runByDefault bool + name string } -func (p *testPass) Name() string { return p.name } -func (p *testPass) RunByDefault() bool { return p.runByDefault } +func (p *testPass) Name() string { return p.name } func (p *testPass) Process(cfg *di.Config) (*di.Config, error) { return cfg, nil } -func makePass(name string, runByDefault bool) *testPass { - return &testPass{name: name, runByDefault: runByDefault} +func makePass(name string) *testPass { + return &testPass{name: name} } func TestPassConfig_ResolvePasses(t *testing.T) { cases := []struct { - name string - enabled map[string]struct{} - passes []di.SelectablePass - wantNames []string + name string + enabled map[string]struct{} + passes []di.Pass + selectablePasses []di.Pass + wantNames []string }{ { - name: "default-on pass is included", - passes: []di.SelectablePass{makePass("a", true)}, + name: "always-included pass is included", + passes: []di.Pass{makePass("a")}, wantNames: []string{"a"}, }, { - name: "default-off pass is excluded", - passes: []di.SelectablePass{makePass("a", false)}, - wantNames: []string{}, + name: "selectable pass without enable flag is excluded", + selectablePasses: []di.Pass{makePass("a")}, + wantNames: []string{}, }, { - name: "default-off pass included when enabled", - enabled: map[string]struct{}{"a": {}}, - passes: []di.SelectablePass{makePass("a", false)}, - wantNames: []string{"a"}, + name: "selectable pass with enable flag is included", + enabled: map[string]struct{}{"a": {}}, + selectablePasses: []di.Pass{makePass("a")}, + wantNames: []string{"a"}, }, { - name: "duplicate pass name runs only once", - passes: []di.SelectablePass{makePass("a", true), makePass("a", true)}, + name: "duplicate always-included pass name runs only once", + passes: []di.Pass{makePass("a"), makePass("a")}, wantNames: []string{"a"}, }, + { + name: "duplicate selectable pass name runs only once", + enabled: map[string]struct{}{"a": {}}, + selectablePasses: []di.Pass{makePass("a"), makePass("a")}, + wantNames: []string{"a"}, + }, + { + name: "always-included pass wins over selectable pass with same name", + enabled: map[string]struct{}{"a": {}}, + passes: []di.Pass{makePass("a")}, + selectablePasses: []di.Pass{makePass("a")}, + wantNames: []string{"a"}, + }, + { + name: "selectable passes are appended after always-included passes", + enabled: map[string]struct{}{"b": {}}, + passes: []di.Pass{makePass("a")}, + selectablePasses: []di.Pass{makePass("b")}, + wantNames: []string{"a", "b"}, + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { pc := PassConfig{Enabled: tc.enabled} - result, err := pc.resolvePasses(tc.passes) + result, err := pc.resolvePasses(tc.passes, tc.selectablePasses) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -69,20 +88,26 @@ func TestPassConfig_ResolvePasses(t *testing.T) { func TestPassConfig_ResolvePasses_Errors(t *testing.T) { cases := []struct { - name string - enabled map[string]struct{} - passes []di.SelectablePass + name string + enabled map[string]struct{} + passes []di.Pass + selectablePasses []di.Pass }{ { - name: "unknown name in --enable-pass", - enabled: map[string]struct{}{"unknown": {}}, - passes: []di.SelectablePass{makePass("foo", true)}, + name: "unknown name in --enable-pass", + enabled: map[string]struct{}{"unknown": {}}, + selectablePasses: []di.Pass{makePass("foo")}, + }, + { + name: "always-included pass is not selectable", + enabled: map[string]struct{}{"foo": {}}, + passes: []di.Pass{makePass("foo")}, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { pc := PassConfig{Enabled: tc.enabled} - _, err := pc.resolvePasses(tc.passes) + _, err := pc.resolvePasses(tc.passes, tc.selectablePasses) if err == nil { t.Error("expected error, got nil") } diff --git a/config.go b/config.go index 8a0e4f6..0d5676c 100644 --- a/config.go +++ b/config.go @@ -13,14 +13,6 @@ type Pass interface { Process(cfg *Config) (*Config, error) } -// SelectablePass extends Pass with a default execution policy that CLI flags -// can override: RunByDefault()=true runs by default; -// RunByDefault()=false skips unless --enable-pass= is set. -type SelectablePass interface { - Pass - RunByDefault() bool -} - // ApplyPasses applies compiler passes sequentially to the config. // Each pass receives the result of the previous pass. func ApplyPasses(cfg *Config, passes []Pass) (*Config, error) { diff --git a/doc/custom-passes.md b/doc/custom-passes.md index c2ab1bc..bb58dde 100644 --- a/doc/custom-passes.md +++ b/doc/custom-passes.md @@ -67,29 +67,18 @@ type Pass interface { - Returns an error if transformation fails - **Must not modify the input config** (create a copy if needed) -## Optional CLI Passes +## CLI Passes -Custom generator binaries built with `cmd.Run` register optional passes. Optional passes implement `Pass` plus `RunByDefault`: +Custom generator binaries built with `cmd.Run` or `cmd.MustRun` register two types of passes: -```go -package di - -type SelectablePass interface { - Pass - RunByDefault() bool -} -``` - -`RunByDefault` controls how the pass participates in CLI filtering: +- **Always-included passes**: Passed as the first `passes` parameter, always run +- **Selectable passes**: Passed as the second `selectablePasses` parameter, filtered by `--enable-pass` flag -- Return `true` to run the pass by default. -- Return `false` to skip the pass unless the user passes `--enable-pass=`. -- Pass names come from `Name()`. -- If the same pass name is registered more than once, only the first included pass runs. +Pass names come from `Name()`. If the same pass name is registered more than once, only the first included pass runs. -`cmd.Run` validates pass flags before generation and returns an error if a name passed to `--enable-pass` does not match any registered pass. +`cmd.Run` validates pass flags before generation and returns an error if a name passed to `--enable-pass` does not match any registered selectable pass. -Use `di.Pass` when calling `di.ApplyPasses` or `cmd.Generate` directly. Use `di.SelectablePass` when registering passes with `cmd.Run` or `cmd.MustRun`. +Use `di.Pass` when calling `di.ApplyPasses`, `cmd.Generate`, `cmd.Run`, or `cmd.MustRun`. ## Creating a Pass @@ -110,10 +99,6 @@ func (p *AutoTagPass) Name() string { return "auto-tag" } -func (p *AutoTagPass) RunByDefault() bool { - return true -} - func (p *AutoTagPass) Process(cfg *di.Config) (*di.Config, error) { // Transform cfg // Return modified config or error @@ -219,14 +204,18 @@ import ( ) func main() { - // Define custom compiler passes - customPasses := []di.SelectablePass{ + // Define always-included custom passes + customPasses := []di.Pass{ &passes.AutoTagPass{}, + } + + // Define selectable passes (filtered by --enable-pass flag) + selectablePasses := []di.Pass{ &passes.ValidationPass{}, } // Run gendi with custom passes - if err := cmd.Run(flag.CommandLine, customPasses); err != nil { + if err := cmd.Run(flag.CommandLine, customPasses, selectablePasses); err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(1) } @@ -239,10 +228,10 @@ func main() { # Build custom generator go build -o bin/gendi ./tools/gendi -# Run custom generator +# Run custom generator (AutoTagPass always runs) ./bin/gendi --config=gendi.yaml --out=./di --pkg=di -# Enable a default-disabled optional pass +# Enable ValidationPass via flag ./bin/gendi --config=gendi.yaml --out=./di --pkg=di --enable-pass=validation # Or use go run @@ -523,7 +512,7 @@ customPasses := []di.Pass{ } ``` -If these passes are registered with `cmd.Run`, use `[]di.SelectablePass` and implement `RunByDefault` on each pass. +If these passes are registered with `cmd.Run`, use `[]di.Pass`. ## Complete Example diff --git a/doc/spec/cli.md b/doc/spec/cli.md index c10f05a..339c56a 100644 --- a/doc/spec/cli.md +++ b/doc/spec/cli.md @@ -16,7 +16,7 @@ gendi | `--container` | Container struct name | | `--strict` | Enable strict validation (default: true) | | `--build-tags` | Go build tags | -| `--enable-pass` | Enable an optional compiler pass by name; repeat for multiple passes; errors on unknown name | +| `--enable-pass` | Enable a selectable compiler pass by name; repeat for multiple passes; errors on unknown name or if pass is not registered as selectable | | `--verbose` | Verbose logging | ## go:generate diff --git a/examples/custom-pass/README.md b/examples/custom-pass/README.md index 080d00e..49c2192 100644 --- a/examples/custom-pass/README.md +++ b/examples/custom-pass/README.md @@ -21,7 +21,6 @@ examples/custom-pass/ │ │ └── gendi.yaml # Service definitions │ └── di/ # Custom compiler passes │ ├── autotag_pass.go -│ └── slog_pass.go ``` ## Custom Passes @@ -68,20 +67,16 @@ This pass demonstrates **variadic function support** - `slog.Logger.With(args .. ### 1. Define Custom Passes -Implement the `di.OptionalPass` interface for passes registered with `cmd.Run`: +Implement the `di.Pass` interface for project-specific behavior. This example defines `AutoTagPass` locally and reuses `stdlib.SLogPass` for structured logger wiring: ```go -type SLogPass struct{} +type AutoTagPass struct{} -func (s *SLogPass) Name() string { - return "slog" +func (p *AutoTagPass) Name() string { + return "auto-tag" } -func (s *SLogPass) RunByDefault() bool { - return true -} - -func (s *SLogPass) Process(cfg *di.Config) (*di.Config, error) { +func (p *AutoTagPass) Process(cfg *di.Config) (*di.Config, error) { // Transform config and return modified version return cfg, nil } @@ -92,12 +87,13 @@ func (s *SLogPass) Process(cfg *di.Config) (*di.Config, error) { `tools/gendi/main.go`: ```go func main() { - passes := []gendi.OptionalPass{ + // Always-included passes + passes := []gendi.Pass{ &di.AutoTagPass{}, - &di.SLogPass{}, + &stdlib.SLogPass{}, } - cmd.MustRun(flag.CommandLine, passes) + cmd.MustRun(flag.CommandLine, passes, nil) } ``` @@ -135,10 +131,6 @@ The `cmd/main.go` includes a go:generate directive: # Generate the container with custom passes go run ./tools/gendi --config=./cmd/gendi.yaml --out=./cmd --pkg=main -# Disable a default-enabled optional pass -# To disable the SLog pass: -go run ./tools/gendi --config=./cmd/gendi.yaml --out=./cmd --pkg=main - # Run the demo go run ./cmd/*.go ``` diff --git a/examples/custom-pass/internal/di/autotag_pass.go b/examples/custom-pass/internal/di/autotag_pass.go index f4ad959..82eb7c8 100644 --- a/examples/custom-pass/internal/di/autotag_pass.go +++ b/examples/custom-pass/internal/di/autotag_pass.go @@ -15,10 +15,6 @@ func (p *AutoTagPass) Name() string { return "auto-tag" } -func (p *AutoTagPass) RunByDefault() bool { - return true -} - func (p *AutoTagPass) Process(cfg *di.Config) (*di.Config, error) { for id, svc := range cfg.Services { if strings.HasPrefix(id, "stdlib.") { diff --git a/examples/custom-pass/tools/gendi/main.go b/examples/custom-pass/tools/gendi/main.go index 9acdd1e..ad58909 100644 --- a/examples/custom-pass/tools/gendi/main.go +++ b/examples/custom-pass/tools/gendi/main.go @@ -11,10 +11,10 @@ import ( func main() { // Register custom compiler passes - passes := []gendi.SelectablePass{ + passes := []gendi.Pass{ &di.AutoTagPass{}, - stdlib.NewSLogPass(true), + &stdlib.SLogPass{}, } - cmd.MustRun(flag.CommandLine, passes) + cmd.MustRun(flag.CommandLine, passes, nil) } diff --git a/public_pass.go b/public_pass.go index 255b5ed..7edb507 100644 --- a/public_pass.go +++ b/public_pass.go @@ -17,10 +17,6 @@ func (p *ExposeAllPass) Name() string { return "expose-all" } -func (p *ExposeAllPass) RunByDefault() bool { - return false -} - func (p *ExposeAllPass) Process(cfg *Config) (*Config, error) { for id, svc := range cfg.Services { svc.Public = true diff --git a/stdlib/README.md b/stdlib/README.md index 7f508c8..586f67b 100644 --- a/stdlib/README.md +++ b/stdlib/README.md @@ -551,7 +551,7 @@ services: ## Compiler Passes -The stdlib package also provides optional compiler passes for use in custom generator binaries. +The stdlib package also provides compiler passes for use in custom generator binaries. ### SLogPass @@ -562,21 +562,31 @@ Automatically wires structured logging into services that follow the slog naming ```go import ( "flag" + gendi "github.com/asp24/gendi" "github.com/asp24/gendi/cmd" "github.com/asp24/gendi/stdlib" ) func main() { - passes := []gendi.OptionalPass{ - stdlib.NewSLogPass(true), // true = run by default + // Always-included passes + passes := []gendi.Pass{ + &stdlib.SLogPass{}, } - cmd.MustRun(flag.CommandLine, passes) + cmd.MustRun(flag.CommandLine, passes, nil) } ``` -`NewSLogPass(runByDefault bool)` controls whether the pass runs unless explicitly toggled via CLI: -- `true` — runs by default -- `false` — skipped unless the user passes `--enable-pass=slog` +To make SLogPass selectable via `--enable-pass=slog`, put it in the second parameter: + +```go +func main() { + passes := []gendi.Pass{} + selectablePasses := []gendi.Pass{ + &stdlib.SLogPass{}, + } + cmd.MustRun(flag.CommandLine, passes, selectablePasses) +} +``` ## See Also diff --git a/stdlib/slog_pass.go b/stdlib/slog_pass.go index 323aa15..e6e6cef 100644 --- a/stdlib/slog_pass.go +++ b/stdlib/slog_pass.go @@ -6,22 +6,12 @@ import ( di "github.com/asp24/gendi" ) -type SLogPass struct { - runByDefault bool -} - -func NewSLogPass(runByDefault bool) *SLogPass { - return &SLogPass{runByDefault: runByDefault} -} +type SLogPass struct{} func (s *SLogPass) Name() string { return "slog" } -func (s *SLogPass) RunByDefault() bool { - return s.runByDefault -} - func (s *SLogPass) getTagAttributes(svc *di.Service) (map[string]any, bool) { for _, tag := range svc.Tags { if !strings.EqualFold(tag.Name, s.Name()) { From 718bd44bf81454d0daac95b33ca6731d2ee11065 Mon Sep 17 00:00:00 2001 From: asp24 <488588+asp24@users.noreply.github.com> Date: Wed, 27 May 2026 12:02:19 +0200 Subject: [PATCH 11/12] Fix style nits in pass_config and ExposeAllPass --- cmd/pass_config.go | 2 +- public_pass.go | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/cmd/pass_config.go b/cmd/pass_config.go index faf5f41..705f990 100644 --- a/cmd/pass_config.go +++ b/cmd/pass_config.go @@ -12,7 +12,7 @@ type PassConfig struct { Enabled map[string]struct{} // Pass names to enable } -// validate Returns an error if any name in Enabled does not match a selectable pass. +// validate returns an error if any name in Enabled does not match a selectable pass. func (pc *PassConfig) validate(selectablePasses []di.Pass) error { known := make(map[string]struct{}, len(selectablePasses)) for _, p := range selectablePasses { diff --git a/public_pass.go b/public_pass.go index 7edb507..24db38c 100644 --- a/public_pass.go +++ b/public_pass.go @@ -1,17 +1,15 @@ package di -// ExposeAllPass is a test-only pass that promotes every service to public, -// causing the generator to emit a public getter for each one. Use it when -// building a test DI container that needs direct access to all services -// regardless of how they are declared in the YAML config. +// ExposeAllPass promotes every service to public, causing the generator to emit +// a public getter for each one. Intended for test containers that need direct +// access to all services regardless of how they are declared in the YAML config. // // Enable via: --enable-pass=expose-all // -// Not intended for production containers — it overrides explicit `public: false` +// Avoid using in production containers — it overrides explicit `public: false` // declarations and disables unreachable-service pruning (all services become // reachable roots), so every imported service gets a generated getter. -type ExposeAllPass struct { -} +type ExposeAllPass struct{} func (p *ExposeAllPass) Name() string { return "expose-all" From 38c0b3f56170259c24fdf54a0274250c60111f42 Mon Sep 17 00:00:00 2001 From: asp24 <488588+asp24@users.noreply.github.com> Date: Wed, 27 May 2026 12:08:32 +0200 Subject: [PATCH 12/12] Flatten PassConfig into Config --- cmd/cli.go | 2 +- cmd/config.go | 59 +++++++++++++++-- cmd/{pass_config_test.go => config_test.go} | 12 ++-- cmd/pass_config.go | 71 --------------------- 4 files changed, 62 insertions(+), 82 deletions(-) rename cmd/{pass_config_test.go => config_test.go} (90%) delete mode 100644 cmd/pass_config.go diff --git a/cmd/cli.go b/cmd/cli.go index bf027b6..db55bb7 100644 --- a/cmd/cli.go +++ b/cmd/cli.go @@ -73,7 +73,7 @@ func Run(flags *flag.FlagSet, passes, selectablePasses []di.Pass) error { return fmt.Errorf("parse flags: %w", err) } - resolvedPasses, err := cfg.Passes.resolvePasses(passes, selectablePasses) + resolvedPasses, err := cfg.resolvePasses(passes, selectablePasses) if err != nil { return fmt.Errorf("resolve passes: %w", err) } diff --git a/cmd/config.go b/cmd/config.go index 064f01f..beae42b 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -4,14 +4,15 @@ import ( "flag" "fmt" + di "github.com/asp24/gendi" "github.com/asp24/gendi/pipeline" ) // Config holds CLI configuration type Config struct { - ConfigPath string - Options pipeline.Options - Passes PassConfig + ConfigPath string + Options pipeline.Options + EnabledPasses map[string]struct{} } func (c *Config) RegisterFlags(flags *flag.FlagSet) { @@ -22,7 +23,57 @@ func (c *Config) RegisterFlags(flags *flag.FlagSet) { flags.BoolVar(&c.Options.Strict, "strict", true, "Enable strict validation") flags.StringVar(&c.Options.BuildTags, "build-tags", "", "Go build tags") flags.BoolVar(&c.Options.Verbose, "verbose", false, "Verbose logging") - c.Passes.RegisterFlags(flags) + flags.Var(&stringSetFlag{values: &c.EnabledPasses}, "enable-pass", "Enable a specific compiler pass (can be specified multiple times)") +} + +func (c *Config) validatePasses(selectablePasses []di.Pass) error { + known := make(map[string]struct{}, len(selectablePasses)) + for _, p := range selectablePasses { + known[p.Name()] = struct{}{} + } + + for name := range c.EnabledPasses { + if _, ok := known[name]; !ok { + return fmt.Errorf("--enable-pass: unknown pass %q", name) + } + } + + return nil +} + +func (c *Config) resolvePasses(passes, selectablePasses []di.Pass) ([]di.Pass, error) { + if err := c.validatePasses(selectablePasses); err != nil { + return nil, err + } + + result := make([]di.Pass, 0, len(passes)+len(selectablePasses)) + included := make(map[string]struct{}, len(passes)+len(selectablePasses)) + for _, p := range passes { + name := p.Name() + if _, ok := included[name]; ok { + continue + } + + result = append(result, p) + included[name] = struct{}{} + } + + for _, p := range selectablePasses { + name := p.Name() + if _, ok := included[name]; ok { + continue + } + + _, enabled := c.EnabledPasses[name] + if !enabled { + continue + } + + result = append(result, p) + included[name] = struct{}{} + } + + return result, nil } // Finalize validates and finalizes the configuration diff --git a/cmd/pass_config_test.go b/cmd/config_test.go similarity index 90% rename from cmd/pass_config_test.go rename to cmd/config_test.go index 910522c..86e44a8 100644 --- a/cmd/pass_config_test.go +++ b/cmd/config_test.go @@ -17,7 +17,7 @@ func makePass(name string) *testPass { return &testPass{name: name} } -func TestPassConfig_ResolvePasses(t *testing.T) { +func TestConfig_ResolvePasses(t *testing.T) { cases := []struct { name string enabled map[string]struct{} @@ -69,8 +69,8 @@ func TestPassConfig_ResolvePasses(t *testing.T) { } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - pc := PassConfig{Enabled: tc.enabled} - result, err := pc.resolvePasses(tc.passes, tc.selectablePasses) + cfg := Config{EnabledPasses: tc.enabled} + result, err := cfg.resolvePasses(tc.passes, tc.selectablePasses) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -86,7 +86,7 @@ func TestPassConfig_ResolvePasses(t *testing.T) { } } -func TestPassConfig_ResolvePasses_Errors(t *testing.T) { +func TestConfig_ResolvePasses_Errors(t *testing.T) { cases := []struct { name string enabled map[string]struct{} @@ -106,8 +106,8 @@ func TestPassConfig_ResolvePasses_Errors(t *testing.T) { } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - pc := PassConfig{Enabled: tc.enabled} - _, err := pc.resolvePasses(tc.passes, tc.selectablePasses) + cfg := Config{EnabledPasses: tc.enabled} + _, err := cfg.resolvePasses(tc.passes, tc.selectablePasses) if err == nil { t.Error("expected error, got nil") } diff --git a/cmd/pass_config.go b/cmd/pass_config.go deleted file mode 100644 index 705f990..0000000 --- a/cmd/pass_config.go +++ /dev/null @@ -1,71 +0,0 @@ -package cmd - -import ( - "flag" - "fmt" - - di "github.com/asp24/gendi" -) - -// PassConfig holds enabled pass configuration. -type PassConfig struct { - Enabled map[string]struct{} // Pass names to enable -} - -// validate returns an error if any name in Enabled does not match a selectable pass. -func (pc *PassConfig) validate(selectablePasses []di.Pass) error { - known := make(map[string]struct{}, len(selectablePasses)) - for _, p := range selectablePasses { - known[p.Name()] = struct{}{} - } - - for name := range pc.Enabled { - if _, ok := known[name]; !ok { - return fmt.Errorf("--enable-pass: unknown pass %q", name) - } - } - - return nil -} - -// resolvePasses builds the final list of passes, applying enable filtering to -// selectable passes and deduplicating by pass name. -func (pc *PassConfig) resolvePasses(passes, selectablePasses []di.Pass) ([]di.Pass, error) { - if err := pc.validate(selectablePasses); err != nil { - return nil, err - } - - result := make([]di.Pass, 0, len(passes)+len(selectablePasses)) - included := make(map[string]struct{}, len(passes)+len(selectablePasses)) - for _, p := range passes { - name := p.Name() - if _, ok := included[name]; ok { - continue - } - - result = append(result, p) - included[name] = struct{}{} - } - - for _, p := range selectablePasses { - name := p.Name() - if _, ok := included[name]; ok { - continue - } - - _, enabled := pc.Enabled[name] - if !enabled { - continue - } - - result = append(result, p) - included[name] = struct{}{} - } - - return result, nil -} - -// RegisterFlags adds pass enable flags to the flag set. -func (pc *PassConfig) RegisterFlags(flags *flag.FlagSet) { - flags.Var(&stringSetFlag{values: &pc.Enabled}, "enable-pass", "Enable a specific compiler pass (can be specified multiple times)") -}