From 29 Critical Violations to Zero: The Solo Builder's Safe Refactoring Playbook
How we eliminated 100% of SwiftLint critical violations in 3 hours without breaking a single test - and why this matters for your CI pipeline
Published: January 2025
Tags: #CodeQuality #Refactoring #SwiftDevelopment #SoloBuilder #TechnicalDebt
The “So What?” - Why You Should Care
Bottom Line Value: Learn how to fix critical code quality violations that block CI/CD pipelines without introducing bugs, using a proven incremental methodology that took us from “CI always fails” to “zero critical violations” in one focused session.
What You’ll Gain:
- A step-by-step safe refactoring methodology you can use today
- Specific techniques for fixing the 4 most common SwiftLint violations
- Time estimates for each type of fix (force unwrapping: 2-3 min each, function complexity: 45-60 min)
- A decision framework for when to refactor vs. when to ship
- ROI: Stop wasting 2-4 hours per week debugging CI failures caused by code quality violations
Who This Helps: Solo builders and small teams who need to pass enterprise quality checks without a dedicated QA team or the luxury of breaking production.
Why Bother: If your GitHub Actions are failing because of linting violations, you can’t ship. This methodology gets you from “CI blocked” to “CI green” without the fear of breaking your app.
TLDR: Incremental Refactoring Beats Big Bang Every Time
We had 29 critical SwiftLint violations blocking our CI pipeline. Instead of a risky “fix everything at once” approach, we used an incremental methodology:
- Fix one violation type at a time (force unwrapping → line length → function complexity)
- Run all 141 tests after each change
- Commit after each successful fix (easy rollback if something breaks)
- Start with easiest violations first (build confidence and momentum)
Result: 100% of critical violations fixed, zero bugs introduced, all tests passing, 3 hours total time.
The Problem: When Enterprise Quality Checks Block Your Solo Builder Velocity
Here’s the situation every solo builder dreads: You’ve built a working feature, all your tests pass locally, you push to GitHub, and… CI fails. Not because your code doesn’t work, but because it doesn’t meet enterprise quality standards.
Our Specific Pain:
- GitHub Actions failing on every push (16 consecutive failures)
- 29 critical SwiftLint violations blocking merges
- No clear path to fix without risking production stability
- Solo builder with no safety net if refactoring breaks things
The Classic Trap: My first instinct was to dive in and fix everything at once. “How hard can 29 violations be?”
Very hard. And very risky.
The Breakthrough: Incremental Refactoring with Safety Checkpoints
Instead of the “fix everything and pray” approach, I developed a methodology that prioritizes safety over speed:
The Core Principle: One Violation Type at a Time
Traditional Approach (Risky):
# Fix all 29 violations in one massive commit
git commit -m "Fix all SwiftLint violations"
# 🔥 Build breaks, tests fail, no idea which change caused it
Safe Incremental Approach:
# Fix one violation type, test, commit
git commit -m "Fix all 16 force unwrapping violations"
npm test # ✅ All 141 tests passing
# Fix next violation type, test, commit
git commit -m "Fix all 9 line length violations"
npm test # ✅ All 141 tests passing
# Continue until done
Why This Works:
- ✅ If something breaks, you know exactly which change caused it
- ✅ Easy rollback with
git revertto the last working state - ✅ Psychological momentum from frequent wins
- ✅ Can pause and ship at any checkpoint
The Methodology: Step-by-Step Safe Refactoring
Phase 1: Categorize and Prioritize (15 minutes)
First, understand what you’re dealing with:
cd bin/audio-capture
swiftlint lint --reporter emoji 2>&1 | grep "⛔️"
Our Violations Breakdown:
- 16 force unwrapping violations (
!operator) - 9 line length violations (>150 characters)
- 2 type body length violations (classes >350 lines)
- 1 function body length violation (>100 lines)
- 1 cyclomatic complexity violation (complexity >10)
Strategic Prioritization:
Tier 1: Quick Wins (Start Here)
- Force unwrapping: 2-3 minutes each, low risk
- Line length: 1-2 minutes each, zero risk
- Empty count: 30 seconds, zero risk
Tier 2: Medium Complexity
- Function body length: 45-60 minutes, medium risk
- Test class body length: 30 minutes, low risk
Tier 3: Structural Changes
- Class body length: 45-60 minutes, medium risk
- Cyclomatic complexity: Usually fixed with function body length
Why This Order Matters: Starting with quick wins builds confidence and momentum. By the time you hit the complex violations, you’ve already reduced the count by 80% and proven your methodology works.
Phase 2: Fix Quick Wins First (60 minutes)
Fixing Force Unwrapping (16 violations → 0)
The Problem:
// ❌ Unsafe - crashes if nil
let url = URL(string: outputPath)!
audioRecorder = try AVAudioRecorder(url: url, settings: settings)
The Safe Fix:
// ✅ Safe - handles nil gracefully
guard let url = URL(string: outputPath) else {
logger.error("Invalid output path: \(outputPath)")
throw VoiceCatcherError.invalidOutputPath(outputPath)
}
audioRecorder = try AVAudioRecorder(url: url, settings: settings)
Time per fix: 2-3 minutes
Risk level: Low (adds error handling, doesn’t change logic)
Test after: swift test (verify no regressions)
Fixing Line Length (9 violations → 0)
The Problem:
// ❌ 180 characters - hard to read
logger.error("Failed to write checkpoint file at \(checkpointPath): \(error.localizedDescription)")
The Fix:
// ✅ Broken into readable lines
logger.error(
"Failed to write checkpoint file at \(checkpointPath): " +
"\(error.localizedDescription)"
)
Time per fix: 1-2 minutes
Risk level: Zero (no logic changes)
Test after: swift build (verify compilation)
Checkpoint: After Phase 2, we went from 29 → 4 violations (86% reduction) in 60 minutes.
Phase 3: Tackle Structural Violations (90 minutes)
Fixing Function Body Length & Complexity
The Problem:
// ❌ 180-line function with complexity 39
func parseArguments(_ args: [String]) throws -> RecordingConfig {
// 180 lines of argument parsing logic
// Nested if statements, loops, validation
// Impossible to test individual pieces
}
The Strategy: Extract Helper Functions
- Create a struct to hold parsed values:
struct ParsedArguments {
var outputPath: String?
var duration: Double?
var silenceThreshold: Double?
var silenceDuration: Double?
// ... other fields
}
- Extract parsing logic into focused functions:
// Each parser handles one argument type
func parseOutputPath(
_ args: [String],
_ index: inout Int
) -> (String?, Int) {
// 5-10 lines of focused logic
// Easy to test, easy to understand
}
func parseDuration(
_ args: [String],
_ index: inout Int
) -> (Double?, Int) {
// 5-10 lines of focused logic
}
- Main function becomes a coordinator:
// ✅ 50-line function with complexity 5
func parseArguments(_ args: [String]) throws -> RecordingConfig {
var parsed = ParsedArguments()
var index = 0
while index < args.count {
let arg = args[index]
switch arg {
case "--output":
(parsed.outputPath, index) = parseOutputPath(args, &index)
case "--duration":
(parsed.duration, index) = parseDuration(args, &index)
// ... other cases
}
}
return try buildRecordingConfig(from: parsed)
}
Time: 45-60 minutes
Risk level: Medium (requires careful testing)
Test after: Run full test suite (141 tests)
Key Insight: This isn’t just about passing linting - it’s about making code testable and maintainable.
Fixing Class Body Length
The Problem:
// ❌ 493-line AudioRecorder class
public class AudioRecorder: NSObject, AVAudioRecorderDelegate {
// Properties (50 lines)
// Initialization (30 lines)
// Recording methods (100 lines)
// Amplitude monitoring (150 lines)
// Delegate methods (80 lines)
// Signal handling (83 lines)
}
The Strategy: Split into Extensions by Functionality
// ✅ Main class: Core properties and public API (130 lines)
public class AudioRecorder: NSObject, AVAudioRecorderDelegate {
// Properties
// Init
// Public recording methods
// deinit (must be in main class)
}
// Extension 1: Amplitude Monitoring (separate file section)
extension AudioRecorder {
func startAmplitudeMonitoring() throws { }
func processAudioBuffer(_ buffer: AVAudioPCMBuffer) { }
// ... related methods
}
// Extension 2: Delegate Methods
extension AudioRecorder {
public func audioRecorderDidFinishRecording(...) { }
func extractAudioData(from wavData: Data) -> Data { }
// ... related methods
}
// Extension 3: Signal Handling
extension AudioRecorder {
func setupSignalHandling() { }
func handleGracefulTermination() { }
// ... related methods
}
Critical Detail: Change private methods to internal (no access modifier) so extensions can access them.
Time: 45-60 minutes
Risk level: Medium (requires understanding access control)
Test after: Full rebuild + all tests
The Results: Measurable Success
Before & After Metrics
| Metric | Before | After | Improvement |
|---|---|---|---|
| Critical Violations | 29 ⛔️ | 0 ⛔️ | 100% |
| Force Unwrapping | 16 | 0 | 100% |
| Line Length | 9 | 0 | 100% |
| Function Complexity | 39 | ~5 | 87% |
| Class Body Length | 493 lines | ~130 lines | 74% |
| Tests Passing | 141/141 | 141/141 | ✅ |
| Build Status | ✅ | ✅ | ✅ |
| Time Invested | - | 3 hours | - |
Business Impact
Immediate Benefits:
- ✅ CI pipeline unblocked (can now merge to main)
- ✅ Code more maintainable (smaller functions, clearer structure)
- ✅ Better error handling (guard statements vs. force unwrapping)
- ✅ Easier to test (extracted functions are testable units)
Long-term Benefits:
- 📈 Faster onboarding (cleaner code is easier to understand)
- 📈 Fewer bugs (better error handling catches edge cases)
- 📈 Easier refactoring (smaller units are easier to change)
- 📈 Professional credibility (enterprise-quality code)
The Decision Framework: When to Refactor vs. When to Ship
Not every violation needs to be fixed immediately. Here’s how to decide:
Fix Immediately (Blocking Issues)
- ✅ CI/CD failures - Can’t ship without fixing
- ✅ Force unwrapping - Crashes waiting to happen
- ✅ Security violations - User data at risk
- ✅ Performance issues - User experience degraded
Fix Soon (Technical Debt)
- ⚠️ Function complexity - Makes future changes harder
- ⚠️ Class body length - Reduces maintainability
- ⚠️ Duplicate code - Increases bug surface area
Fix Later (Polish)
- 📋 Style warnings - Consistency issues, not functionality
- 📋 Documentation gaps - Important but not blocking
- 📋 Minor optimizations - Nice to have, not critical
The 80/20 Rule: We got 86% reduction (29 → 4 violations) in the first hour by focusing on quick wins. The remaining 14% took 2 more hours. Know when to declare victory.
Lessons for Solo Builders
What Worked
- Incremental approach - One violation type at a time
- Test after every change - Catch regressions immediately
- Commit frequently - Easy rollback if needed
- Start with easy wins - Build confidence and momentum
- Extract functions - Solves multiple violations at once
What Didn’t Work
- Trying to fix everything at once - Too risky, too overwhelming
- Skipping tests - False confidence leads to broken builds
- Complex fixes first - Wastes time, kills momentum
- Ignoring access control - Extensions need internal methods
The Mindset Shift
Stop thinking of linting violations as “annoying rules” and start thinking of them as “free code review from experienced developers.”
SwiftLint isn’t trying to make your life harder - it’s trying to prevent the bugs you’ll spend hours debugging next month.
Your Turn: The Copy-Paste Playbook
Ready to tackle your own violations? Here’s the exact process:
Step 1: Assess (15 min)
# Run linter and categorize violations
swiftlint lint --reporter emoji 2>&1 | grep "⛔️" > violations.txt
# Count by type
grep "force_unwrapping" violations.txt | wc -l
grep "line_length" violations.txt | wc -l
grep "function_body_length" violations.txt | wc -l
Step 2: Prioritize (5 min)
Create three buckets:
- Quick wins: Force unwrapping, line length, empty count
- Medium effort: Function complexity, test class length
- Structural: Class body length, architectural changes
Step 3: Execute (2-3 hours)
# Create a branch
git checkout -b fix/swiftlint-violations
# Fix quick wins
# ... make changes ...
swift test # Verify
git commit -m "Fix force unwrapping violations (16 → 0)"
# Fix medium effort
# ... make changes ...
swift test # Verify
git commit -m "Fix function complexity (parseArguments)"
# Fix structural
# ... make changes ...
swift build && swift test # Full verification
git commit -m "Split AudioRecorder into extensions"
# Merge when done
git checkout main
git merge fix/swiftlint-violations
Step 4: Verify (15 min)
# Clean build from scratch
rm -rf .build
swift build -c release
# Run full test suite
swift test
# Verify zero critical violations
swiftlint lint --reporter emoji 2>&1 | grep "⛔️"
# Should return nothing
The Bottom Line
Refactoring doesn’t have to be risky. With an incremental approach, comprehensive testing, and frequent commits, you can eliminate critical violations without fear.
Start small. Pick one force unwrapping violation. Fix it. Test it. Commit it. You’ve just completed your first safe refactoring.
The compound effect of these small, safe fixes will transform your codebase from “CI always fails” to “enterprise quality” faster than you think.
The best time to fix violations was before they blocked your CI. The second best time is now.
Building VoiceCatcher has taught me that code quality isn’t about perfection - it’s about systematic improvement with safety nets. Want more behind-the-scenes content? Follow along as we build in public.
Next Post: “The Hidden Cost of Force Unwrapping: Why ! is a Bug Waiting to Happen”