Skip to content
Back to News
guide
refactoring
best-practices

Finding Dead Code Before It Ships

By CKB Team

Dead code is a silent tax on every codebase. It confuses developers, bloats bundles, and creates false positives in searches. CKB helps you find it before it ships.

The Dead Code Problem

Code becomes "dead" in several ways:

  • Unused functions - Written for a feature that was removed
  • Unreachable branches - Conditions that can never be true
  • Deprecated APIs - Old versions kept for compatibility that no one uses
  • Copy-paste remnants - Duplicated code where the original was deleted

Traditional static analysis catches some of this, but misses code that could be called but isn't in practice.

CKB's Two-Pronged Approach

CKB combines static and dynamic analysis:

  1. Static analysis (findDeadCode) - Code with no callers in the codebase
  2. Runtime analysis (getObservedUsage) - Code that exists but is never executed

Together, they give you confidence that code is truly dead.

Finding Statically Dead Code

User: "Find unused functions in the payment module"

AI: [calls findDeadCode for internal/payment]

"Found 7 potentially unused functions:

High confidence (no callers):
- formatLegacyReceipt() in receipt.go:145
- validateOldChecksum() in validation.go:89
- migrateV1Transaction() in migration.go:34

Medium confidence (only test callers):
- mockPaymentProvider() in helpers.go:23
- createTestTransaction() in helpers.go:67

Low confidence (reflection/dynamic calls possible):
- handleWebhookV1() in webhooks.go:123
- processLegacyRefund() in refund.go:78"

Understanding Confidence Levels

CKB assigns confidence based on how the code might be called:

High confidence - No references anywhere. Safe to remove.

Medium confidence - Only called from tests. The tested code might be dead, or tests might be covering internal helpers.

Low confidence - No static callers, but the function name suggests it might be called dynamically (reflection, webhooks, event handlers).

Checking Runtime Usage

Static analysis has limits. A function might have callers but those callers are also dead. Or code might be called via configuration or reflection.

getObservedUsage checks against runtime telemetry (if configured):

User: "Is handleWebhookV1 actually used in production?"

AI: [calls getObservedUsage for handleWebhookV1]

"Runtime analysis for handleWebhookV1:

Last 30 days:
- Invocations: 0
- Last seen: 2025-08-14 (5 months ago)

Caller analysis:
- webhookRouter calls this for 'v1' webhook type
- No v1 webhooks have been registered since migration

Verdict: Safe to remove. V1 webhook format is deprecated
and no active integrations use it."

Setting Up Runtime Tracking

CKB can integrate with your observability stack:

// .ckb/config.json
{
  "observedUsage": {
    "enabled": true,
    "source": "datadog",  // or "prometheus", "custom"
    "functionMetric": "function.invocations",
    "lookbackDays": 30
  }
}

Without runtime integration, CKB uses git history heuristics (last modification date, recent callers) to estimate usage likelihood.

Workflow: Cleaning Up Dead Code

Step 1: Scan the Codebase

User: "Find all dead code in our codebase"

AI: [calls findDeadCode with full scope]

"Dead code analysis complete.

Summary:
- 23 unused functions
- 8 unused types/structs
- 12 unused constants
- 156 unused imports (auto-fixable)

Estimated savings: ~2,400 lines of code (3.2% of codebase)

Would you like me to break this down by module?"

Step 2: Prioritize by Impact

User: "Which dead code should I remove first?"

AI: [analyzes dead code by impact]

"Priority ranking:

1. internal/legacy/ - 890 lines, entire module unused
   Impact: Reduces bundle size, removes confusing old patterns
   Risk: Low - no callers, clear deprecation comments

2. pkg/utils/deprecated.go - 234 lines
   Impact: 12 functions that duplicate stdlib
   Risk: Low - wrapper functions with modern replacements

3. internal/payment/v1/ - 456 lines
   Impact: Old payment processing, migrated 8 months ago
   Risk: Medium - verify no v1 API consumers remain

4. Scattered functions - 820 lines across 34 files
   Impact: Incremental cleanup
   Risk: Varies - review individually"

Step 3: Verify Before Removing

User: "Is it safe to delete internal/legacy/?"

AI: [calls prepareChange with delete]
    [calls getObservedUsage for all exports]

"Safety analysis for deleting internal/legacy/:

Static analysis: No internal callers ✓
Runtime analysis: Zero invocations in 90 days ✓
External consumers: None (internal package) ✓
Test dependencies: 3 test files import this module

Action required:
- Delete internal/legacy/
- Delete internal/legacy_test.go
- Update test files: payment_test.go, auth_test.go, user_test.go

This is safe to remove."

Step 4: Clean Up

User: "Generate a PR description for removing the legacy module"

AI: "## Remove unused legacy module

### Summary
Removes `internal/legacy/` which has been unused since the v2 migration.

### Changes
- Delete `internal/legacy/` (890 lines)
- Delete associated tests (156 lines)
- Update 3 test files that imported legacy helpers

### Verification
- CKB static analysis: No callers
- Runtime telemetry: Zero invocations in 90 days
- Manual review: Confirmed all functionality migrated to v2

### Impact
- Reduces codebase by ~1,000 lines
- Removes confusing deprecated patterns
- Slightly faster build times"

Common Dead Code Patterns

Feature Flags Never Removed

User: "Find feature flags that are always on"

AI: [searches for feature flag patterns]
    [calls getObservedUsage]

"Found 4 feature flags that are always enabled:

- ENABLE_NEW_CHECKOUT (enabled 2024-03-15, 22 months ago)
- USE_V2_AUTH (enabled 2024-08-01, 17 months ago)
- FAST_SEARCH (enabled 2025-01-01, 12 months ago)
- NEW_DASHBOARD (enabled 2025-06-01, 7 months ago)

The first three are safe to remove - the 'old' code paths
are completely dead."

Compatibility Shims

User: "Find backwards compatibility code we can remove"

AI: [searches for compatibility patterns]

"Found 8 compatibility shims:

Safe to remove (no consumers):
- convertV1Response() - V1 API deprecated 18 months ago
- legacyDateFormat() - Old mobile app discontinued
- shimOldWebhook() - Webhook v1 migration complete

Need verification:
- supportOldBrowser() - Check analytics for IE11 traffic
- legacyEncryption() - May affect stored data"

Prevention: Catching Dead Code Early

Pre-commit Hook

# .ckb/hooks/pre-commit
ckb check dead-code --staged --fail-on-new

CI Integration

# .github/workflows/dead-code.yml
- name: Check for dead code
  run: |
    ckb findDeadCode --format=json > dead-code.json
    if [ $(jq '.high_confidence | length' dead-code.json) -gt 0 ]; then
      echo "New dead code detected!"
      exit 1
    fi

Results

Teams using CKB for dead code detection typically find:

  • 2-5% of their codebase is unused
  • 30% faster onboarding (less confusing code)
  • Smaller bundle sizes
  • Faster test runs (fewer dead code paths to cover)

Links: