833 lines
22 KiB
Markdown
833 lines
22 KiB
Markdown
# Pre-Commit Review Examples
|
|
|
|
Real-world examples of design review findings with before/after comparisons.
|
|
|
|
## Example 0: Detection Workflow
|
|
|
|
Shows how the pre-commit-review skill analyzes code step-by-step using LLM reasoning.
|
|
|
|
### Input: user/service.go
|
|
|
|
```go
|
|
package user
|
|
|
|
type UserService struct {
|
|
Repo Repository
|
|
}
|
|
|
|
func (s *UserService) CreateUser(id string, email string) error {
|
|
// Validate ID
|
|
if id == "" {
|
|
return errors.New("empty id")
|
|
}
|
|
|
|
// Validate email
|
|
if !strings.Contains(email, "@") {
|
|
return errors.New("invalid email")
|
|
}
|
|
|
|
// Open DB
|
|
db, err := sql.Open("postgres", connStr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer db.Close()
|
|
|
|
// Build query
|
|
query := "INSERT INTO users (id, email) VALUES (?, ?)"
|
|
_, err = db.Exec(query, id, email)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Send email
|
|
_ = sendEmail(email, "Welcome!")
|
|
|
|
return nil
|
|
}
|
|
```
|
|
|
|
### LLM Analysis Step-by-Step
|
|
|
|
**Step 1: Apply Primitive Obsession Check (reference.md #1)**
|
|
|
|
Questions LLM asks:
|
|
- Q: Are primitives being validated?
|
|
- A: Yes - 'id' string validated at line 9, 'email' string validated at line 14
|
|
|
|
- Q: Is same validation repeated elsewhere?
|
|
- A: [Uses Grep tool to search codebase]
|
|
- Found in handler.go:23 (same id validation)
|
|
- Found in repository.go:45 (same email validation)
|
|
|
|
- Q: Would types add value beyond validation?
|
|
- A: Yes
|
|
- Email: Complex validation pattern, domain concept
|
|
- UserID: Validation scattered, type safety benefit
|
|
|
|
- Q: Calculate juiciness score (see reference.md):
|
|
```
|
|
UserID:
|
|
- Validation in 3 places (+1)
|
|
- Simple empty check (+0)
|
|
- Used in 8 locations (+2)
|
|
Score: 3/10 → MEDIUM priority
|
|
|
|
Email:
|
|
- Validation in 3 places (+1)
|
|
- Regex validation possible (+3)
|
|
- Domain/LocalPart methods (+2)
|
|
- Used in 8 locations (+2)
|
|
Score: 8/10 → HIGH priority
|
|
```
|
|
|
|
**Finding identified:**
|
|
```
|
|
🔴 Design Debt: Primitive obsession
|
|
- string id → UserID type (MEDIUM juiciness)
|
|
- string email → Email type (HIGH juiciness)
|
|
```
|
|
|
|
---
|
|
|
|
**Step 2: Apply Storifying Check (reference.md #2)**
|
|
|
|
Questions LLM asks:
|
|
- Q: Does function read like a story?
|
|
- A: No - mixes abstraction levels:
|
|
- Lines 7-16: High-level validation
|
|
- Lines 18-23: Low-level DB connection setup
|
|
- Lines 25-31: SQL implementation details
|
|
- Lines 33-34: High-level notification
|
|
|
|
- Q: Can I explain flow in 3-5 clear steps?
|
|
- A: Should be: validateUser() → saveUser() → notifyUser()
|
|
But currently mixes infrastructure and business logic
|
|
|
|
**Finding identified:**
|
|
```
|
|
🟡 Readability Debt: Function not storified
|
|
Mixed abstraction levels in CreateUser
|
|
Should extract: validateUser(), saveUser(), notifyUser()
|
|
```
|
|
|
|
---
|
|
|
|
**Step 3: Apply Self-Validating Types Check (reference.md #3)**
|
|
|
|
Questions LLM asks:
|
|
- Q: Do methods check if struct fields are nil/invalid?
|
|
- A: No checks in this method, but...
|
|
|
|
- Q: Could UserService.Repo be nil?
|
|
- A: Yes - struct can be created without validation
|
|
```go
|
|
svc := &UserService{} // Repo is nil!
|
|
svc.CreateUser(...) // Will panic if we use Repo
|
|
```
|
|
|
|
- Q: Is there a constructor with validation?
|
|
- A: [Searches codebase] No NewUserService constructor found
|
|
|
|
**Finding identified:**
|
|
```
|
|
🔴 Design Debt: Non-self-validating type
|
|
UserService can be created with nil Repo
|
|
Need NewUserService constructor
|
|
```
|
|
|
|
---
|
|
|
|
**Step 4: Apply Design Bugs Check (reference.md #8)**
|
|
|
|
Questions LLM asks:
|
|
- Q: Potential nil dereferences?
|
|
- A: Not obvious in this function (but found UserService.Repo issue above)
|
|
|
|
- Q: Are errors ignored?
|
|
- A: YES! Line 34: `_ = sendEmail(email, "Welcome!")`
|
|
Silent failure, no visibility into delivery failures
|
|
|
|
- Q: Resources cleaned up?
|
|
- A: Yes, defer db.Close() at line 23 ✓
|
|
|
|
- Q: Context propagated?
|
|
- A: No ctx parameter, but no downstream calls needing it
|
|
|
|
**Finding identified:**
|
|
```
|
|
🐛 Bug: Ignored error
|
|
Line 34: _ = sendEmail() silently ignores failure
|
|
Should at least log the error
|
|
```
|
|
|
|
---
|
|
|
|
**Step 5: Apply Other Checks**
|
|
|
|
- Abstraction Levels (#4): Already covered in storifying
|
|
- Architecture (#5): Single file, can't assess pattern
|
|
- Naming (#6): Names are clear and idiomatic ✓
|
|
- Testing (#7): No test file in scope
|
|
- Comment Quality: Missing (no package docs, no function docs)
|
|
- File Structure: Single type, reasonable size ✓
|
|
|
|
---
|
|
|
|
### Analysis Complete - Categorize & Prioritize
|
|
|
|
**Bugs (Priority 1 - Fix immediately):**
|
|
1. Ignored error at line 34
|
|
|
|
**Design Debt (Priority 2 - Fix before commit):**
|
|
2. Primitive obsession: string email (HIGH juiciness: 8/10)
|
|
3. Primitive obsession: string id (MEDIUM juiciness: 3/10)
|
|
4. Non-self-validating: UserService
|
|
|
|
**Readability Debt (Priority 3 - Improves maintainability):**
|
|
5. Not storified: CreateUser function
|
|
|
|
---
|
|
|
|
### Output Report
|
|
|
|
```
|
|
📊 CODE REVIEW REPORT
|
|
Generated: 2025-11-09 15:45:00
|
|
Scope: user/service.go (1 file, 40 lines)
|
|
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
SUMMARY
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
Total findings: 5
|
|
🐛 Bugs: 1 (fix immediately)
|
|
🔴 Design Debt: 3 (fix before commit recommended)
|
|
🟡 Readability Debt: 1 (improves maintainability)
|
|
🟢 Polish: 0
|
|
|
|
Estimated fix effort: 50 minutes total
|
|
- Critical (bugs + high juiciness design): 25 min
|
|
- Recommended (medium design + readability): 25 min
|
|
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
🐛 BUGS (1) - FIX IMMEDIATELY
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
1. Ignored error
|
|
Location: user/service.go:34
|
|
Code: _ = sendEmail(email, "Welcome!")
|
|
|
|
Issue: Email sending failure silently ignored
|
|
Impact: No visibility into delivery failures, hard to debug
|
|
|
|
Fix: Log error at minimum:
|
|
if err := sendEmail(email, "Welcome!"); err != nil {
|
|
log.Printf("failed to send welcome email: %v", err)
|
|
}
|
|
|
|
Better: Return error if critical:
|
|
if err := sendEmail(email, "Welcome!"); err != nil {
|
|
return fmt.Errorf("send welcome email: %w", err)
|
|
}
|
|
|
|
Effort: 2 min
|
|
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
🔴 DESIGN DEBT (3) - FIX BEFORE COMMIT RECOMMENDED
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
1. Primitive obsession: string email (HIGH juiciness: 8/10)
|
|
Locations: Line 6, 14, 28, 34
|
|
Also found in: handler.go:23, repository.go:45
|
|
|
|
Juiciness Score: 8/10
|
|
- Validation in 3 places (+1)
|
|
- Complex regex validation possible (+3)
|
|
- Methods: Domain(), LocalPart() (+2)
|
|
- Used in 8 locations (+2)
|
|
|
|
Current:
|
|
func CreateUser(id string, email string) error {
|
|
if !strings.Contains(email, "@") {
|
|
return errors.New("invalid email")
|
|
}
|
|
// ...
|
|
}
|
|
|
|
Better:
|
|
type Email string
|
|
|
|
func ParseEmail(s string) (Email, error) {
|
|
if !emailRegex.MatchString(s) {
|
|
return "", fmt.Errorf("invalid email: %s", s)
|
|
}
|
|
return Email(s), nil
|
|
}
|
|
|
|
func (e Email) Domain() string { /* ... */ }
|
|
func (e Email) LocalPart() string { /* ... */ }
|
|
func (e Email) String() string { return string(e) }
|
|
|
|
func CreateUser(id string, email Email) error {
|
|
// No validation needed, guaranteed valid
|
|
}
|
|
|
|
Why: Type safety, centralized validation, prevents invalid emails
|
|
Fix: Use @code-designing skill → Create Email type
|
|
Effort: 20 min
|
|
|
|
2. Primitive obsession: string id (MEDIUM juiciness: 3/10)
|
|
Locations: Line 6, 9, 28
|
|
Also found in: handler.go:23, repository.go:45
|
|
|
|
Juiciness Score: 3/10
|
|
- Validation in 3 places (+1)
|
|
- Simple empty check (+0)
|
|
- Used in 8 locations (+2)
|
|
|
|
Note: Borderline case. Judgment call on whether to create type.
|
|
|
|
Better:
|
|
type UserID string
|
|
|
|
func ParseUserID(s string) (UserID, error) {
|
|
if s == "" {
|
|
return "", errors.New("empty user id")
|
|
}
|
|
return UserID(s), nil
|
|
}
|
|
|
|
Why: Centralizes validation, type safety
|
|
Fix: Use @code-designing skill → Create UserID type
|
|
Effort: 10 min
|
|
|
|
3. Non-self-validating type: UserService
|
|
Location: user/service.go:4
|
|
|
|
Issue: UserService.Repo is public, can be nil
|
|
No constructor to validate dependencies
|
|
|
|
Current:
|
|
type UserService struct {
|
|
Repo Repository // Can be nil!
|
|
}
|
|
|
|
svc := &UserService{} // Invalid state allowed
|
|
|
|
Better:
|
|
type UserService struct {
|
|
repo Repository // Private
|
|
}
|
|
|
|
func NewUserService(repo Repository) (*UserService, error) {
|
|
if repo == nil {
|
|
return nil, errors.New("repo required")
|
|
}
|
|
return &UserService{repo: repo}, nil
|
|
}
|
|
|
|
func (s *UserService) CreateUser(id string, email Email) error {
|
|
// No nil checks needed - constructor guarantees validity
|
|
return s.repo.Save(...)
|
|
}
|
|
|
|
Why: Impossible to create invalid service, eliminates defensive checks
|
|
Fix: Use @code-designing skill → Add constructor
|
|
Effort: 10 min
|
|
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
🟡 READABILITY DEBT (1) - IMPROVES MAINTAINABILITY
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
1. Function not storified: CreateUser
|
|
Location: user/service.go:6-36
|
|
|
|
Issue: Mixes 3 abstraction levels:
|
|
- Lines 7-16: High-level validation
|
|
- Lines 18-31: Low-level DB connection/SQL
|
|
- Lines 33-34: High-level notification
|
|
|
|
Flow not clear at a glance, hard to test pieces independently.
|
|
|
|
Better:
|
|
func CreateUser(id string, email Email) error {
|
|
if err := validateUser(id, email); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := saveUser(id, email); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := notifyUser(email); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateUser(id string, email Email) error { /* ... */ }
|
|
func saveUser(id string, email Email) error { /* ... */ }
|
|
func notifyUser(email Email) error { /* ... */ }
|
|
|
|
Why: Reads like a story, testable pieces, clear intent
|
|
Fix: Use @refactoring skill → Storifying pattern
|
|
Effort: 15 min
|
|
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
RECOMMENDATIONS
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
Priority 1: Fix immediately (2 min)
|
|
☐ Fix ignored error (log or return)
|
|
|
|
Priority 2: Fix before commit (40 min)
|
|
☐ Create Email type (HIGH juiciness) @code-designing
|
|
☐ Create UserID type (MEDIUM juiciness) @code-designing
|
|
☐ Add NewUserService constructor @code-designing
|
|
|
|
Priority 3: Improve maintainability (15 min)
|
|
☐ Storify CreateUser function @refactoring
|
|
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
SKILLS TO USE
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
@code-designing: For creating Email, UserID types and NewUserService
|
|
@refactoring: For storifying CreateUser function
|
|
Manual: For fixing ignored error (simple change)
|
|
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
END OF REPORT
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
```
|
|
|
|
This example demonstrates how the reviewer skill applies the complete checklist from reference.md systematically, using LLM reasoning to detect issues that linters cannot catch.
|
|
|
|
---
|
|
|
|
## Example 1: Primitive Obsession + Self-Validating Types
|
|
|
|
### Before (Design Debt 🔴)
|
|
```go
|
|
package user
|
|
|
|
type UserService struct {
|
|
DB *sql.DB // Might be nil
|
|
}
|
|
|
|
func (s *UserService) CreateUser(id string, email string) error {
|
|
// Defensive check
|
|
if s.DB == nil {
|
|
return errors.New("db is nil")
|
|
}
|
|
|
|
// Primitive validation
|
|
if id == "" {
|
|
return errors.New("id required")
|
|
}
|
|
if !strings.Contains(email, "@") {
|
|
return errors.New("invalid email")
|
|
}
|
|
|
|
// Business logic
|
|
_, err := s.DB.Exec("INSERT INTO users (id, email) VALUES ($1, $2)", id, email)
|
|
return err
|
|
}
|
|
```
|
|
|
|
**Review Findings:**
|
|
- 🔴 **Design Debt**: Primitive obsession on `id` and `email`
|
|
- 🔴 **Design Debt**: Non-self-validating type (`UserService.DB` might be nil)
|
|
|
|
### After (No Debt)
|
|
```go
|
|
package user
|
|
|
|
type UserID string
|
|
type Email string
|
|
|
|
func NewUserID(s string) (UserID, error) {
|
|
if s == "" {
|
|
return "", errors.New("id required")
|
|
}
|
|
return UserID(s), nil
|
|
}
|
|
|
|
func NewEmail(s string) (Email, error) {
|
|
if !strings.Contains(s, "@") {
|
|
return "", errors.New("invalid email")
|
|
}
|
|
return Email(s), nil
|
|
}
|
|
|
|
type UserService struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
func NewUserService(db *sql.DB) (*UserService, error) {
|
|
if db == nil {
|
|
return nil, errors.New("db is required")
|
|
}
|
|
return &UserService{db: db}, nil
|
|
}
|
|
|
|
func (s *UserService) CreateUser(id UserID, email Email) error {
|
|
// No validation needed - types guarantee validity
|
|
_, err := s.db.Exec("INSERT INTO users (id, email) VALUES ($1, $2)", id, email)
|
|
return err
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Example 2: Mixed Abstraction Levels + Storifying
|
|
|
|
### Before (Readability Debt 🟡)
|
|
```go
|
|
func ProcessPayment(orderID string, amount float64) error {
|
|
// High-level: validation
|
|
if orderID == "" {
|
|
return errors.New("invalid order id")
|
|
}
|
|
if amount <= 0 {
|
|
return errors.New("invalid amount")
|
|
}
|
|
|
|
// Low-level: HTTP client setup
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
req, err := http.NewRequest("POST", "https://api.payment.com/charge", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+os.Getenv("API_KEY"))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
// Low-level: JSON marshaling
|
|
body := map[string]interface{}{
|
|
"order_id": orderID,
|
|
"amount": amount,
|
|
}
|
|
jsonBody, err := json.Marshal(body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Body = io.NopCloser(bytes.NewReader(jsonBody))
|
|
|
|
// Low-level: HTTP call
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// High-level: logging
|
|
log.Printf("Payment processed for order %s", orderID)
|
|
return nil
|
|
}
|
|
```
|
|
|
|
**Review Findings:**
|
|
- 🟡 **Readability Debt**: Mixed abstraction levels (business + HTTP details)
|
|
- 🟡 **Readability Debt**: Function not storified (hard to see flow)
|
|
|
|
### After (No Debt)
|
|
```go
|
|
func ProcessPayment(orderID OrderID, amount Amount) error {
|
|
if err := validatePayment(orderID, amount); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := chargePaymentGateway(orderID, amount); err != nil {
|
|
return err
|
|
}
|
|
|
|
logPaymentSuccess(orderID)
|
|
return nil
|
|
}
|
|
|
|
func validatePayment(orderID OrderID, amount Amount) error {
|
|
// Validation logic only (already validated by types, but could have business rules)
|
|
return nil
|
|
}
|
|
|
|
func chargePaymentGateway(orderID OrderID, amount Amount) error {
|
|
// HTTP client logic encapsulated
|
|
client := newPaymentClient()
|
|
return client.Charge(orderID, amount)
|
|
}
|
|
|
|
func logPaymentSuccess(orderID OrderID) {
|
|
log.Printf("Payment processed for order %s", orderID)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Example 3: Horizontal Layers → Vertical Slices
|
|
|
|
### Before (Design Debt 🔴)
|
|
```
|
|
project/
|
|
├── domain/
|
|
│ └── user.go
|
|
├── service/
|
|
│ └── user_service.go
|
|
├── repository/
|
|
│ └── user_repository.go
|
|
└── handler/
|
|
└── user_handler.go
|
|
```
|
|
|
|
**Review Finding:**
|
|
- 🔴 **Design Debt**: Horizontal layering instead of vertical slices
|
|
- Impact: User feature changes require touching 4 different directories
|
|
|
|
### After (No Debt)
|
|
```
|
|
project/
|
|
└── user/
|
|
├── user.go # Domain type
|
|
├── service.go # Business logic
|
|
├── repository.go # Persistence
|
|
├── handler.go # HTTP
|
|
└── user_test.go
|
|
```
|
|
|
|
**Benefits:**
|
|
- All user-related code in one place
|
|
- Easy to understand complete feature
|
|
- Independent testing/deployment
|
|
|
|
---
|
|
|
|
## Example 4: Generic Naming
|
|
|
|
### Before (Readability Debt 🟡)
|
|
```go
|
|
package common
|
|
|
|
type DataManager struct {
|
|
store Storage
|
|
}
|
|
|
|
func (m *DataManager) ProcessData(data interface{}) (interface{}, error) {
|
|
// ...
|
|
}
|
|
|
|
func HandleRequest(ctx context.Context, data map[string]interface{}) error {
|
|
// ...
|
|
}
|
|
```
|
|
|
|
**Review Findings:**
|
|
- 🟡 **Readability Debt**: Generic package name (`common`)
|
|
- 🟡 **Readability Debt**: Vague type name (`DataManager`)
|
|
- 🟡 **Readability Debt**: Meaningless function names (`ProcessData`, `HandleRequest`)
|
|
|
|
### After (No Debt)
|
|
```go
|
|
package user
|
|
|
|
type Service struct {
|
|
repo Repository
|
|
}
|
|
|
|
func (s *Service) Create(ctx context.Context, u User) error {
|
|
// ...
|
|
}
|
|
|
|
func (s *Service) Authenticate(ctx context.Context, credentials Credentials) (Token, error) {
|
|
// ...
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Example 5: Testing Anti-Patterns
|
|
|
|
### Before (Design Debt 🔴)
|
|
```go
|
|
package user // Same package
|
|
|
|
// Testing private function
|
|
func TestValidateEmailInternal(t *testing.T) {
|
|
assert.True(t, validateEmailInternal("test@example.com"))
|
|
}
|
|
|
|
// Heavy mocking
|
|
func TestCreateUser(t *testing.T) {
|
|
mockRepo := &MockRepository{}
|
|
mockEmailer := &MockEmailer{}
|
|
|
|
mockRepo.On("Save", mock.Anything).Return(nil)
|
|
mockEmailer.On("Send", mock.Anything).Return(nil)
|
|
|
|
svc := &UserService{
|
|
Repo: mockRepo,
|
|
Emailer: mockEmailer,
|
|
}
|
|
|
|
err := svc.CreateUser("123", "test@example.com")
|
|
assert.NoError(t, err)
|
|
|
|
mockRepo.AssertExpectations(t)
|
|
}
|
|
|
|
// Flaky with time.Sleep
|
|
func TestAsyncOperation(t *testing.T) {
|
|
go doAsyncWork()
|
|
time.Sleep(100 * time.Millisecond) // ❌ Flaky
|
|
assert.True(t, workCompleted)
|
|
}
|
|
```
|
|
|
|
**Review Findings:**
|
|
- 🔴 **Design Debt**: Testing private methods
|
|
- 🔴 **Design Debt**: Using mocks instead of real implementations
|
|
- 🔴 **Design Debt**: Flaky test with time.Sleep
|
|
|
|
### After (No Debt)
|
|
```go
|
|
package user_test // External package
|
|
|
|
// Test public API only
|
|
func TestService_CreateUser(t *testing.T) {
|
|
// Use real implementations
|
|
repo := user.NewInMemoryRepository()
|
|
emailer := user.NewTestEmailer()
|
|
|
|
svc, err := user.NewUserService(repo, emailer)
|
|
require.NoError(t, err)
|
|
|
|
id, _ := user.NewUserID("123")
|
|
email, _ := user.NewEmail("test@example.com")
|
|
|
|
u := user.User{ID: id, Email: email}
|
|
err = svc.CreateUser(context.Background(), u)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
// Verify via public API
|
|
retrieved, err := svc.GetUser(context.Background(), id)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, email, retrieved.Email)
|
|
}
|
|
|
|
// No flakiness with channels
|
|
func TestAsyncOperation(t *testing.T) {
|
|
done := make(chan struct{})
|
|
|
|
go func() {
|
|
doAsyncWork()
|
|
close(done)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
// Success
|
|
case <-time.After(1 * time.Second):
|
|
t.Fatal("timeout waiting for async work")
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Example 6: Complete Commit Review Output
|
|
|
|
### Scenario
|
|
Developer adds user authentication feature with some design issues.
|
|
|
|
### Review Output
|
|
```
|
|
📋 COMMIT READINESS SUMMARY
|
|
|
|
✅ Linter: Passed (0 issues)
|
|
✅ Tests: 87% coverage (5 new types, 23 test cases)
|
|
⚠️ Design Review: 5 findings (see below)
|
|
|
|
🎯 COMMIT SCOPE
|
|
Modified:
|
|
- user/service.go (+120, -30 lines)
|
|
- user/auth.go (new file, +85 lines)
|
|
|
|
Added:
|
|
- user/user_id.go (new type: UserID)
|
|
- user/password.go (new type: Password)
|
|
|
|
Tests:
|
|
- user/service_test.go (+95 lines)
|
|
- user/auth_test.go (new, +140 lines)
|
|
|
|
⚠️ DESIGN REVIEW FINDINGS
|
|
|
|
🔴 DESIGN DEBT (Recommended to fix):
|
|
|
|
1. user/service.go:67 - Primitive obsession on session token
|
|
Current: func CreateSession(userID UserID) (string, error)
|
|
Better: func CreateSession(userID UserID) (SessionToken, error)
|
|
Why: Session tokens should be validated types to prevent empty/invalid tokens
|
|
Fix: Use @code-designing to create SessionToken type with validation
|
|
|
|
2. user/auth.go:34 - Non-self-validating type
|
|
Current:
|
|
type Authenticator struct {
|
|
HashCost int // Could be invalid
|
|
}
|
|
Better:
|
|
func NewAuthenticator(hashCost int) (*Authenticator, error) {
|
|
if hashCost < 4 || hashCost > 31 {
|
|
return nil, errors.New("invalid hash cost")
|
|
}
|
|
// ...
|
|
}
|
|
Why: Constructor should validate, methods shouldn't need defensive checks
|
|
Fix: Use @code-designing to add validating constructor
|
|
|
|
🟡 READABILITY DEBT (Consider fixing):
|
|
|
|
3. user/auth.go:89 - Mixed abstraction levels in Authenticate()
|
|
Function mixes high-level auth flow with low-level bcrypt details
|
|
Why: Harder to understand auth logic at a glance
|
|
Fix: Use @refactoring to extract password comparison to separate function
|
|
|
|
4. user/service.go:45 - Function could be storified better
|
|
Current: validateAndCreateUser() does validation + creation in one function
|
|
Better: Split into validateUser() and createUser() for clarity
|
|
Why: Single responsibility, easier to test each part
|
|
Fix: Use @refactoring to split responsibilities
|
|
|
|
🟢 POLISH OPPORTUNITIES:
|
|
|
|
5. user/auth.go:12 - Less idiomatic naming
|
|
Current: ComparePasswordWithHash
|
|
Better: PasswordMatches
|
|
Why: More concise, Go-style naming
|
|
|
|
📝 BROADER CONTEXT:
|
|
While reviewing user/service.go, noticed email is still stored as string type
|
|
(line 23). Consider refactoring to use Email type consistently across the file
|
|
for better type safety (similar to UserID change in this commit).
|
|
|
|
💡 SUGGESTED COMMIT MESSAGE
|
|
Add user authentication with self-validating types
|
|
|
|
- Introduce UserID and Password self-validating types
|
|
- Implement Authenticator with bcrypt password hashing
|
|
- Add CreateSession and Authenticate methods
|
|
- Achieve 87% test coverage with real bcrypt testing
|
|
|
|
Follows primitive obsession principles with type-safe IDs and passwords.
|
|
|
|
────────────────────────────────────────
|
|
|
|
Would you like to:
|
|
1. Commit as-is (5 design findings remain)
|
|
2. Fix design debt only (🔴 items 1-2), then commit
|
|
3. Fix design + readability debt (🔴🟡 items 1-4), then commit
|
|
4. Fix all findings including polish (🔴🟡🟢 all items), then commit
|
|
5. Expand scope to refactor email type throughout file, then commit
|
|
```
|