Skip to content

Commit 2144385

Browse files
Copilotintel352
andauthored
fix(modernize): camelcase-config rule false-positives on schema-defined snake_case keys (#309)
* Initial plan * fix: skip schema-defined snake_case keys in camelcase-config modernize rule Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> * fix: cache schema keys per module type and improve test assertion Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> Co-authored-by: Jonathan Langevin <codingsloth@pm.me>
1 parent de3f13f commit 2144385

2 files changed

Lines changed: 89 additions & 1 deletion

File tree

cmd/wfctl/modernize_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,62 @@ modules:
485485
}
486486
}
487487

488+
func TestCamelCaseConfigCheck_SchemaDefinedKeysNotFlagged(t *testing.T) {
489+
// openapi module uses snake_case keys (spec_file, register_routes, swagger_ui,
490+
// max_body_bytes) defined in its own schema — these must NOT be flagged.
491+
input := `
492+
modules:
493+
- name: api-docs
494+
type: openapi
495+
config:
496+
spec_file: ./specs/api.yaml
497+
register_routes: false
498+
swagger_ui:
499+
enabled: true
500+
path: /docs
501+
max_body_bytes: 1048576
502+
`
503+
rule := findRule("camelcase-config")
504+
if rule == nil {
505+
t.Fatal("camelcase-config rule not found")
506+
}
507+
doc := parseTestYAML(t, input)
508+
findings := rule.Check(doc, []byte(input))
509+
if len(findings) != 0 {
510+
t.Errorf("expected no findings for schema-defined snake_case keys, got %d: %v", len(findings), findings)
511+
}
512+
}
513+
514+
func TestCamelCaseConfigCheck_UnknownModuleTypeStillFlagged(t *testing.T) {
515+
// For a module type not in the schema registry, snake_case keys should still be flagged.
516+
input := `
517+
modules:
518+
- name: my-thing
519+
type: custom.unknown
520+
config:
521+
snake_key: value
522+
`
523+
rule := findRule("camelcase-config")
524+
if rule == nil {
525+
t.Fatal("camelcase-config rule not found")
526+
}
527+
doc := parseTestYAML(t, input)
528+
findings := rule.Check(doc, []byte(input))
529+
if len(findings) == 0 {
530+
t.Fatal("expected findings for snake_case config keys on unknown module type")
531+
}
532+
found := false
533+
for _, f := range findings {
534+
if strings.Contains(f.Message, `"snake_key"`) {
535+
found = true
536+
break
537+
}
538+
}
539+
if !found {
540+
t.Errorf("expected a finding mentioning key \"snake_key\", got: %v", findings)
541+
}
542+
}
543+
488544
func TestModernizeAllRulesRegistered(t *testing.T) {
489545
rules := modernize.AllRules()
490546
expectedIDs := []string{

modernize/rules.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"regexp"
66
"strings"
77

8+
"github.com/GoCodeAlone/workflow/schema"
89
"gopkg.in/yaml.v3"
910
)
1011

@@ -625,6 +626,28 @@ func requestParseConfigRule() Rule {
625626
var snakeCaseKeyRegex = regexp.MustCompile(`^[a-z]+(_[a-z0-9]+)+$`)
626627

627628
func camelCaseConfigRule() Rule {
629+
// Build a registry of schema-defined config key names per module type so
630+
// that keys which are intentionally snake_case (e.g. openapi's spec_file,
631+
// register_routes, swagger_ui) are never flagged as anti-patterns.
632+
schemaRegistry := schema.NewModuleSchemaRegistry()
633+
634+
// Cache the schema key sets by module type to avoid rebuilding them for
635+
// every module instance encountered during a Check run.
636+
schemaKeyCache := make(map[string]map[string]bool)
637+
schemaKeysFor := func(moduleType string) map[string]bool {
638+
if cached, ok := schemaKeyCache[moduleType]; ok {
639+
return cached
640+
}
641+
keys := make(map[string]bool)
642+
if ms := schemaRegistry.Get(moduleType); ms != nil {
643+
for i := range ms.ConfigFields {
644+
keys[ms.ConfigFields[i].Key] = true
645+
}
646+
}
647+
schemaKeyCache[moduleType] = keys
648+
return keys
649+
}
650+
628651
return Rule{
629652
ID: "camelcase-config",
630653
Description: "Detect snake_case config field names (engine requires camelCase)",
@@ -641,9 +664,18 @@ func camelCaseConfigRule() Rule {
641664
if nameNode != nil {
642665
modName = nameNode.Value
643666
}
667+
668+
// Resolve the set of officially defined config keys for this module
669+
// type so that schema-declared snake_case keys are not flagged.
670+
var schemaKeys map[string]bool
671+
typeNode := findMapValue(mod, "type")
672+
if typeNode != nil && typeNode.Value != "" {
673+
schemaKeys = schemaKeysFor(typeNode.Value)
674+
}
675+
644676
for i := 0; i+1 < len(cfg.Content); i += 2 {
645677
key := cfg.Content[i]
646-
if key.Kind == yaml.ScalarNode && snakeCaseKeyRegex.MatchString(key.Value) {
678+
if key.Kind == yaml.ScalarNode && snakeCaseKeyRegex.MatchString(key.Value) && !schemaKeys[key.Value] {
647679
findings = append(findings, Finding{
648680
RuleID: "camelcase-config",
649681
Line: key.Line,

0 commit comments

Comments
 (0)