Skip to content

Commit ac9d4dd

Browse files
committed
fix: resolve style naming conflicts causing duplicate type declarations
- Added style conflict detection and cleanup system in internal/fixer/style_conflicts.go - Auto-detect existing style (go_zero vs gozero) before code generation - Clean up conflicting files before generation to prevent duplicates - Validate no conflicts exist after generation - Integrated into create_api_service and generate_from_spec tools - Added comprehensive test suite for style conflict handling Fixes issue where regenerating code with different --style flag would create duplicate files (e.g., service_context.go and servicecontext.go) leading to 'redeclared in this block' compilation errors.
1 parent 42c4cbd commit ac9d4dd

File tree

4 files changed

+362
-2
lines changed

4 files changed

+362
-2
lines changed

internal/fixer/style_conflicts.go

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package fixer
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
)
9+
10+
// StyleConflictPair represents a pair of files that conflict due to different naming styles
11+
type StyleConflictPair struct {
12+
GoZeroStyle string // go_zero style: snake_case (e.g., service_context.go)
13+
GoZeroFlat string // gozero style: flat (e.g., servicecontext.go)
14+
}
15+
16+
// knownStyleConflicts lists all known file pairs that can conflict between go_zero and gozero styles
17+
var knownStyleConflicts = []StyleConflictPair{
18+
{GoZeroStyle: "service_context.go", GoZeroFlat: "servicecontext.go"},
19+
// Add more known conflicts here if discovered
20+
}
21+
22+
// CleanupStyleConflicts removes conflicting files based on the chosen style
23+
// This prevents duplicate type declarations when switching between go_zero and gozero styles
24+
func CleanupStyleConflicts(projectPath string, style string) error {
25+
// Determine which style's files to keep
26+
keepGoZeroStyle := (style == "go_zero")
27+
28+
// Walk through project directories looking for conflicts
29+
err := filepath.Walk(projectPath, func(path string, info os.FileInfo, err error) error {
30+
if err != nil {
31+
// Ignore "no such file" errors - we might have deleted the file during cleanup
32+
if os.IsNotExist(err) {
33+
return nil
34+
}
35+
return err
36+
}
37+
38+
// Skip if not a directory
39+
if !info.IsDir() {
40+
return nil
41+
}
42+
43+
// Check each known conflict pair
44+
for _, conflict := range knownStyleConflicts {
45+
goZeroFile := filepath.Join(path, conflict.GoZeroStyle)
46+
gozeroFile := filepath.Join(path, conflict.GoZeroFlat)
47+
48+
// Check if both files exist (conflict!)
49+
goZeroExists := fileExists(goZeroFile)
50+
gozeroExists := fileExists(gozeroFile)
51+
52+
if goZeroExists && gozeroExists {
53+
// Both exist - remove the one we don't want
54+
var fileToRemove string
55+
if keepGoZeroStyle {
56+
fileToRemove = gozeroFile
57+
} else {
58+
fileToRemove = goZeroFile
59+
}
60+
61+
// Double-check file exists before removing
62+
if fileExists(fileToRemove) {
63+
if err := os.Remove(fileToRemove); err != nil {
64+
return fmt.Errorf("failed to remove conflicting file %s: %w", fileToRemove, err)
65+
}
66+
}
67+
}
68+
}
69+
70+
return nil
71+
})
72+
73+
return err
74+
}
75+
76+
// DetectExistingStyle detects which naming style is currently used in the project
77+
// Returns "go_zero" or "gozero", or empty string if cannot determine
78+
func DetectExistingStyle(projectPath string) string {
79+
for _, conflict := range knownStyleConflicts {
80+
// Check in common locations
81+
commonDirs := []string{
82+
filepath.Join(projectPath, "internal", "svc"),
83+
filepath.Join(projectPath, "internal", "handler"),
84+
filepath.Join(projectPath, "internal", "logic"),
85+
}
86+
87+
for _, dir := range commonDirs {
88+
goZeroFile := filepath.Join(dir, conflict.GoZeroStyle)
89+
gozeroFile := filepath.Join(dir, conflict.GoZeroFlat)
90+
91+
if fileExists(goZeroFile) {
92+
return "go_zero"
93+
}
94+
if fileExists(gozeroFile) {
95+
return "gozero"
96+
}
97+
}
98+
}
99+
100+
return ""
101+
}
102+
103+
// fileExists checks if a file exists
104+
func fileExists(path string) bool {
105+
info, err := os.Stat(path)
106+
if err != nil {
107+
return false
108+
}
109+
return !info.IsDir()
110+
}
111+
112+
// SuggestStyleBasedOnExisting suggests which style to use based on existing files
113+
// Returns the detected style, or the provided default if no existing style detected
114+
func SuggestStyleBasedOnExisting(projectPath string, defaultStyle string) string {
115+
if existingStyle := DetectExistingStyle(projectPath); existingStyle != "" {
116+
return existingStyle
117+
}
118+
return defaultStyle
119+
}
120+
121+
// ValidateNoStyleConflicts checks if there are any style conflicts in the project
122+
// Returns an error if conflicts are found
123+
func ValidateNoStyleConflicts(projectPath string) error {
124+
var conflicts []string
125+
126+
err := filepath.Walk(projectPath, func(path string, info os.FileInfo, err error) error {
127+
if err != nil {
128+
return err
129+
}
130+
131+
if !info.IsDir() {
132+
return nil
133+
}
134+
135+
for _, conflict := range knownStyleConflicts {
136+
goZeroFile := filepath.Join(path, conflict.GoZeroStyle)
137+
gozeroFile := filepath.Join(path, conflict.GoZeroFlat)
138+
139+
if fileExists(goZeroFile) && fileExists(gozeroFile) {
140+
relPath, _ := filepath.Rel(projectPath, path)
141+
conflicts = append(conflicts, fmt.Sprintf("%s: both %s and %s exist",
142+
relPath, conflict.GoZeroStyle, conflict.GoZeroFlat))
143+
}
144+
}
145+
146+
return nil
147+
})
148+
149+
if err != nil {
150+
return err
151+
}
152+
153+
if len(conflicts) > 0 {
154+
return fmt.Errorf("style conflicts detected:\n%s", strings.Join(conflicts, "\n"))
155+
}
156+
157+
return nil
158+
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package fixer_test
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/zeromicro/mcp-zero/internal/fixer"
9+
)
10+
11+
func TestCleanupStyleConflicts(t *testing.T) {
12+
t.Run("cleanup_gozero_when_choosing_go_zero", func(t *testing.T) {
13+
// Create a temporary directory for this subtest
14+
tmpDir, err := os.MkdirTemp("", "style_conflict_test_*")
15+
if err != nil {
16+
t.Fatalf("Failed to create temp dir: %v", err)
17+
}
18+
defer os.RemoveAll(tmpDir)
19+
20+
// Create internal/svc directory
21+
svcDir := filepath.Join(tmpDir, "internal", "svc")
22+
if err := os.MkdirAll(svcDir, 0755); err != nil {
23+
t.Fatalf("Failed to create svc dir: %v", err)
24+
}
25+
26+
// Create both files
27+
goZeroFile := filepath.Join(svcDir, "service_context.go")
28+
gozeroFile := filepath.Join(svcDir, "servicecontext.go")
29+
30+
if err := os.WriteFile(goZeroFile, []byte("package svc\ntype ServiceContext struct{}"), 0644); err != nil {
31+
t.Fatalf("Failed to create go_zero file: %v", err)
32+
}
33+
if err := os.WriteFile(gozeroFile, []byte("package svc\ntype ServiceContext struct{}"), 0644); err != nil {
34+
t.Fatalf("Failed to create gozero file: %v", err)
35+
}
36+
37+
// Run cleanup with go_zero style
38+
if err := fixer.CleanupStyleConflicts(tmpDir, "go_zero"); err != nil {
39+
t.Fatalf("CleanupStyleConflicts failed: %v", err)
40+
}
41+
42+
// Verify go_zero file exists
43+
if _, err := os.Stat(goZeroFile); os.IsNotExist(err) {
44+
t.Error("go_zero style file should exist")
45+
}
46+
47+
// Verify gozero file was removed
48+
if _, err := os.Stat(gozeroFile); !os.IsNotExist(err) {
49+
t.Error("gozero style file should have been removed")
50+
}
51+
})
52+
53+
t.Run("cleanup_go_zero_when_choosing_gozero", func(t *testing.T) {
54+
// Create a temporary directory for this subtest
55+
tmpDir, err := os.MkdirTemp("", "style_conflict_test_*")
56+
if err != nil {
57+
t.Fatalf("Failed to create temp dir: %v", err)
58+
}
59+
defer os.RemoveAll(tmpDir)
60+
61+
// Create internal/svc directory
62+
svcDir := filepath.Join(tmpDir, "internal", "svc")
63+
if err := os.MkdirAll(svcDir, 0755); err != nil {
64+
t.Fatalf("Failed to create svc dir: %v", err)
65+
}
66+
67+
// Create both files again
68+
goZeroFile := filepath.Join(svcDir, "service_context.go")
69+
gozeroFile := filepath.Join(svcDir, "servicecontext.go")
70+
71+
if err := os.WriteFile(goZeroFile, []byte("package svc\ntype ServiceContext struct{}"), 0644); err != nil {
72+
t.Fatalf("Failed to create go_zero file: %v", err)
73+
}
74+
if err := os.WriteFile(gozeroFile, []byte("package svc\ntype ServiceContext struct{}"), 0644); err != nil {
75+
t.Fatalf("Failed to create gozero file: %v", err)
76+
}
77+
78+
// Run cleanup with gozero style
79+
if err := fixer.CleanupStyleConflicts(tmpDir, "gozero"); err != nil {
80+
t.Fatalf("CleanupStyleConflicts failed: %v", err)
81+
}
82+
83+
// Verify go_zero file was removed
84+
if _, err := os.Stat(goZeroFile); !os.IsNotExist(err) {
85+
t.Error("go_zero style file should have been removed")
86+
}
87+
88+
// Verify gozero file exists
89+
if _, err := os.Stat(gozeroFile); os.IsNotExist(err) {
90+
t.Error("gozero style file should exist")
91+
}
92+
})
93+
}
94+
95+
func TestDetectExistingStyle(t *testing.T) {
96+
tmpDir, err := os.MkdirTemp("", "detect_style_test_*")
97+
if err != nil {
98+
t.Fatalf("Failed to create temp dir: %v", err)
99+
}
100+
defer os.RemoveAll(tmpDir)
101+
102+
svcDir := filepath.Join(tmpDir, "internal", "svc")
103+
if err := os.MkdirAll(svcDir, 0755); err != nil {
104+
t.Fatalf("Failed to create svc dir: %v", err)
105+
}
106+
107+
t.Run("detect_go_zero_style", func(t *testing.T) {
108+
goZeroFile := filepath.Join(svcDir, "service_context.go")
109+
if err := os.WriteFile(goZeroFile, []byte("package svc"), 0644); err != nil {
110+
t.Fatalf("Failed to create file: %v", err)
111+
}
112+
113+
style := fixer.DetectExistingStyle(tmpDir)
114+
if style != "go_zero" {
115+
t.Errorf("Expected 'go_zero', got '%s'", style)
116+
}
117+
118+
os.Remove(goZeroFile)
119+
})
120+
121+
t.Run("detect_gozero_style", func(t *testing.T) {
122+
gozeroFile := filepath.Join(svcDir, "servicecontext.go")
123+
if err := os.WriteFile(gozeroFile, []byte("package svc"), 0644); err != nil {
124+
t.Fatalf("Failed to create file: %v", err)
125+
}
126+
127+
style := fixer.DetectExistingStyle(tmpDir)
128+
if style != "gozero" {
129+
t.Errorf("Expected 'gozero', got '%s'", style)
130+
}
131+
132+
os.Remove(gozeroFile)
133+
})
134+
135+
t.Run("detect_no_style", func(t *testing.T) {
136+
style := fixer.DetectExistingStyle(tmpDir)
137+
if style != "" {
138+
t.Errorf("Expected empty string, got '%s'", style)
139+
}
140+
})
141+
}
142+
143+
func TestValidateNoStyleConflicts(t *testing.T) {
144+
tmpDir, err := os.MkdirTemp("", "validate_conflicts_test_*")
145+
if err != nil {
146+
t.Fatalf("Failed to create temp dir: %v", err)
147+
}
148+
defer os.RemoveAll(tmpDir)
149+
150+
svcDir := filepath.Join(tmpDir, "internal", "svc")
151+
if err := os.MkdirAll(svcDir, 0755); err != nil {
152+
t.Fatalf("Failed to create svc dir: %v", err)
153+
}
154+
155+
t.Run("no_conflicts", func(t *testing.T) {
156+
goZeroFile := filepath.Join(svcDir, "service_context.go")
157+
if err := os.WriteFile(goZeroFile, []byte("package svc"), 0644); err != nil {
158+
t.Fatalf("Failed to create file: %v", err)
159+
}
160+
161+
if err := fixer.ValidateNoStyleConflicts(tmpDir); err != nil {
162+
t.Errorf("Expected no error, got: %v", err)
163+
}
164+
165+
os.Remove(goZeroFile)
166+
})
167+
168+
t.Run("with_conflicts", func(t *testing.T) {
169+
goZeroFile := filepath.Join(svcDir, "service_context.go")
170+
gozeroFile := filepath.Join(svcDir, "servicecontext.go")
171+
172+
if err := os.WriteFile(goZeroFile, []byte("package svc"), 0644); err != nil {
173+
t.Fatalf("Failed to create go_zero file: %v", err)
174+
}
175+
if err := os.WriteFile(gozeroFile, []byte("package svc"), 0644); err != nil {
176+
t.Fatalf("Failed to create gozero file: %v", err)
177+
}
178+
179+
if err := fixer.ValidateNoStyleConflicts(tmpDir); err == nil {
180+
t.Error("Expected error for conflicts, got nil")
181+
}
182+
183+
os.Remove(goZeroFile)
184+
os.Remove(gozeroFile)
185+
})
186+
}

tools/create_api_service.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ func CreateAPIService(ctx context.Context, req *mcp.CallToolRequest, params Crea
9797
return responses.FormatError(fmt.Sprintf("failed to update config file: %v", err))
9898
}
9999

100+
// Validate no style conflicts
101+
if err := fixer.ValidateNoStyleConflicts(serviceDir); err != nil {
102+
return responses.FormatError(fmt.Sprintf("style conflicts detected: %v", err))
103+
}
104+
100105
// Verify build
101106
if err := fixer.VerifyBuild(serviceDir); err != nil {
102107
return responses.FormatError(fmt.Sprintf("failed to verify build: %v", err))

tools/generate_from_spec.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,21 @@ func GenerateAPIFromSpec(ctx context.Context, req *mcp.CallToolRequest, params G
5555
return responses.FormatValidationError("output_dir", outputDir, err.Error(), "Provide an absolute path to an existing writable directory")
5656
}
5757

58-
// T043: Set code style (default: go_zero)
58+
// T043: Set code style (default: detect existing or use go_zero)
5959
style := params.Style
6060
if style == "" {
61-
style = "go_zero"
61+
// Try to detect existing style to avoid conflicts
62+
style = fixer.SuggestStyleBasedOnExisting(outputDir, "go_zero")
6263
}
6364
if style != "go_zero" && style != "gozero" {
6465
return responses.FormatValidationError("style", style, "invalid style", "Use 'go_zero' or 'gozero'")
6566
}
6667

68+
// Clean up any existing style conflicts before generating
69+
if err := fixer.CleanupStyleConflicts(outputDir, style); err != nil {
70+
return responses.FormatError(fmt.Sprintf("failed to cleanup style conflicts: %v", err))
71+
}
72+
6773
// T044: Execute goctl api go command
6874
executor, err := goctl.NewExecutor()
6975
if err != nil {
@@ -99,6 +105,11 @@ func GenerateAPIFromSpec(ctx context.Context, req *mcp.CallToolRequest, params G
99105
return responses.FormatError(fmt.Sprintf("failed to tidy Go module: %v", err))
100106
}
101107

108+
// Validate no style conflicts after generation
109+
if err := fixer.ValidateNoStyleConflicts(outputDir); err != nil {
110+
return responses.FormatError(fmt.Sprintf("style conflicts detected after generation: %v", err))
111+
}
112+
102113
// T046: Verify build success
103114
if err := fixer.VerifyBuild(outputDir); err != nil {
104115
return responses.FormatError(fmt.Sprintf("failed to verify build: %v", err))

0 commit comments

Comments
 (0)