diff --git a/cmd/wfctl/modernize_test.go b/cmd/wfctl/modernize_test.go index 7c5be023..dbe2f482 100644 --- a/cmd/wfctl/modernize_test.go +++ b/cmd/wfctl/modernize_test.go @@ -485,6 +485,62 @@ 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") + } + 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) { rules := modernize.AllRules() expectedIDs := []string{ diff --git a/modernize/rules.go b/modernize/rules.go index 174bb135..4ce443b6 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,28 @@ 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() + + // 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)", @@ -641,9 +664,18 @@ func camelCaseConfigRule() Rule { if nameNode != nil { modName = nameNode.Value } + + // 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 != "" { + schemaKeys = schemaKeysFor(typeNode.Value) + } + 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,