From 74f56c445304ed9689e3e62377868654b8d23204 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 11:25:52 +0000 Subject: [PATCH 1/3] Initial plan From efceff658406c5928ba091415fc1acfb8f7c6f8e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 11:35:01 +0000 Subject: [PATCH 2/3] fix: skip schema-defined snake_case keys in camelcase-config modernize rule Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> --- cmd/wfctl/modernize_test.go | 46 +++++++++++++++++++++++++++++++++++++ modernize/rules.go | 21 ++++++++++++++++- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/cmd/wfctl/modernize_test.go b/cmd/wfctl/modernize_test.go index cde75baa..4b0adcf2 100644 --- a/cmd/wfctl/modernize_test.go +++ b/cmd/wfctl/modernize_test.go @@ -483,6 +483,52 @@ modules: } } +func TestCamelCaseConfigCheck_SchemaDefinedKeysNotFlagged(t *testing.T) { + // openapi module uses snake_case keys (spec_file, register_routes, swagger_ui, + // max_body_bytes) defined in its own schema — these must NOT be flagged. + input := ` +modules: + - name: api-docs + type: openapi + config: + spec_file: ./specs/api.yaml + register_routes: false + swagger_ui: + enabled: true + path: /docs + max_body_bytes: 1048576 +` + rule := findRule("camelcase-config") + if rule == nil { + t.Fatal("camelcase-config rule not found") + } + doc := parseTestYAML(t, input) + findings := rule.Check(doc, []byte(input)) + if len(findings) != 0 { + t.Errorf("expected no findings for schema-defined snake_case keys, got %d: %v", len(findings), findings) + } +} + +func TestCamelCaseConfigCheck_UnknownModuleTypeStillFlagged(t *testing.T) { + // For a module type not in the schema registry, snake_case keys should still be flagged. + input := ` +modules: + - name: my-thing + type: custom.unknown + config: + snake_key: value +` + rule := findRule("camelcase-config") + if rule == nil { + t.Fatal("camelcase-config rule not found") + } + doc := parseTestYAML(t, input) + findings := rule.Check(doc, []byte(input)) + if len(findings) == 0 { + t.Fatal("expected findings for snake_case config keys on unknown module type") + } +} + func TestModernizeAllRulesRegistered(t *testing.T) { rules := modernize.AllRules() expectedIDs := []string{ diff --git a/modernize/rules.go b/modernize/rules.go index 174bb135..8f7e0ba4 100644 --- a/modernize/rules.go +++ b/modernize/rules.go @@ -5,6 +5,7 @@ import ( "regexp" "strings" + "github.com/GoCodeAlone/workflow/schema" "gopkg.in/yaml.v3" ) @@ -625,6 +626,11 @@ func requestParseConfigRule() Rule { var snakeCaseKeyRegex = regexp.MustCompile(`^[a-z]+(_[a-z0-9]+)+$`) func camelCaseConfigRule() Rule { + // Build a registry of schema-defined config key names per module type so + // that keys which are intentionally snake_case (e.g. openapi's spec_file, + // register_routes, swagger_ui) are never flagged as anti-patterns. + schemaRegistry := schema.NewModuleSchemaRegistry() + return Rule{ ID: "camelcase-config", Description: "Detect snake_case config field names (engine requires camelCase)", @@ -641,9 +647,22 @@ func camelCaseConfigRule() Rule { if nameNode != nil { modName = nameNode.Value } + + // Build a set of officially defined config keys for this module type + // so that snake_case keys that come from the schema are not flagged. + schemaKeys := make(map[string]bool) + typeNode := findMapValue(mod, "type") + if typeNode != nil && typeNode.Value != "" { + if ms := schemaRegistry.Get(typeNode.Value); ms != nil { + for _, field := range ms.ConfigFields { + schemaKeys[field.Key] = true + } + } + } + for i := 0; i+1 < len(cfg.Content); i += 2 { key := cfg.Content[i] - if key.Kind == yaml.ScalarNode && snakeCaseKeyRegex.MatchString(key.Value) { + if key.Kind == yaml.ScalarNode && snakeCaseKeyRegex.MatchString(key.Value) && !schemaKeys[key.Value] { findings = append(findings, Finding{ RuleID: "camelcase-config", Line: key.Line, From b0249f4eb1140f733ff0dadb6dc86216f2f18199 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:13:11 +0000 Subject: [PATCH 3/3] fix: cache schema keys per module type and improve test assertion Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> --- cmd/wfctl/modernize_test.go | 10 ++++++++++ modernize/rules.go | 29 +++++++++++++++++++++-------- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/cmd/wfctl/modernize_test.go b/cmd/wfctl/modernize_test.go index 4b0adcf2..e2c6ed13 100644 --- a/cmd/wfctl/modernize_test.go +++ b/cmd/wfctl/modernize_test.go @@ -527,6 +527,16 @@ modules: if len(findings) == 0 { t.Fatal("expected findings for snake_case config keys on unknown module type") } + found := false + for _, f := range findings { + if strings.Contains(f.Message, `"snake_key"`) { + found = true + break + } + } + if !found { + t.Errorf("expected a finding mentioning key \"snake_key\", got: %v", findings) + } } func TestModernizeAllRulesRegistered(t *testing.T) { diff --git a/modernize/rules.go b/modernize/rules.go index 8f7e0ba4..4ce443b6 100644 --- a/modernize/rules.go +++ b/modernize/rules.go @@ -631,6 +631,23 @@ func camelCaseConfigRule() Rule { // register_routes, swagger_ui) are never flagged as anti-patterns. schemaRegistry := schema.NewModuleSchemaRegistry() + // Cache the schema key sets by module type to avoid rebuilding them for + // every module instance encountered during a Check run. + schemaKeyCache := make(map[string]map[string]bool) + schemaKeysFor := func(moduleType string) map[string]bool { + if cached, ok := schemaKeyCache[moduleType]; ok { + return cached + } + keys := make(map[string]bool) + if ms := schemaRegistry.Get(moduleType); ms != nil { + for i := range ms.ConfigFields { + keys[ms.ConfigFields[i].Key] = true + } + } + schemaKeyCache[moduleType] = keys + return keys + } + return Rule{ ID: "camelcase-config", Description: "Detect snake_case config field names (engine requires camelCase)", @@ -648,16 +665,12 @@ func camelCaseConfigRule() Rule { modName = nameNode.Value } - // Build a set of officially defined config keys for this module type - // so that snake_case keys that come from the schema are not flagged. - schemaKeys := make(map[string]bool) + // Resolve the set of officially defined config keys for this module + // type so that schema-declared snake_case keys are not flagged. + var schemaKeys map[string]bool typeNode := findMapValue(mod, "type") if typeNode != nil && typeNode.Value != "" { - if ms := schemaRegistry.Get(typeNode.Value); ms != nil { - for _, field := range ms.ConfigFields { - schemaKeys[field.Key] = true - } - } + schemaKeys = schemaKeysFor(typeNode.Value) } for i := 0; i+1 < len(cfg.Content); i += 2 {