| Crates.io | searchdeadcode |
| lib.rs | searchdeadcode |
| version | 0.4.0 |
| created_at | 2025-12-07 01:15:06.753011+00 |
| updated_at | 2025-12-07 01:15:06.753011+00 |
| description | A fast CLI tool to detect and remove dead/unused code in Android projects (Kotlin & Java) |
| homepage | |
| repository | https://github.com/KevinDoremy/SearchDeadCode |
| max_upload_size | |
| id | 1970980 |
| size | 905,684 |
A blazingly fast CLI tool written in Rust to detect and safely remove dead/unused code in Android projects (Kotlin & Java).
Inspired by Periphery for Swift.
| Category | Detection |
|---|---|
| Core | Unused classes, interfaces, methods, functions, properties, fields, imports |
| Advanced | Unused parameters, enum cases, type aliases |
| Smart | Assign-only properties (written but never read), dead branches, redundant public modifiers |
| Android-Aware | Respects Activities, Fragments, XML layouts, Manifest entries as entry points |
| Resources | Unused Android resources (strings, colors, dimens, styles, attrs) |
# Build from source
git clone https://github.com/KevinDoremy/SearchDeadCode
cd searchdeadcode
cargo build --release
# Analyze an Android project
./target/release/searchdeadcode /path/to/android/project
# Dry-run deletion preview
./target/release/searchdeadcode /path/to/project --delete --dry-run
# From source
git clone https://github.com/KevinDoremy/SearchDeadCode
cd searchdeadcode
cargo install --path .
# Or via cargo (when published)
cargo install searchdeadcode
# Analyze current directory
searchdeadcode .
# Analyze specific Android project
searchdeadcode ./my-android-app
# Analyze with verbose output
searchdeadcode ./app --verbose
# Quiet mode (only results)
searchdeadcode ./app --quiet
# Terminal (default) - colored, grouped output
searchdeadcode ./app
# JSON - for programmatic use
searchdeadcode ./app --format json --output report.json
# SARIF - for GitHub Actions / CI integration
searchdeadcode ./app --format sarif --output report.sarif
SearchDeadCode supports hybrid analysis by combining static code analysis with runtime coverage data. This significantly increases confidence in dead code findings and reduces false positives.
# With JaCoCo coverage from CI tests
searchdeadcode ./app --coverage build/reports/jacoco/test/jacocoTestReport.xml
# With Kover coverage (Kotlin)
searchdeadcode ./app --coverage build/reports/kover/report.xml
# With LCOV coverage
searchdeadcode ./app --coverage coverage/lcov.info
# Multiple coverage files (merged)
searchdeadcode ./app \
--coverage build/reports/unit-test.xml \
--coverage build/reports/integration-test.xml
Each finding is assigned a confidence level:
| Level | Indicator | Description |
|---|---|---|
| Confirmed | ● (green) | Runtime coverage confirms code is never executed |
| High | ◉ (bright green) | Private/internal code with no static references |
| Medium | ○ (yellow) | Default for static-only analysis |
| Low | ◌ (red) | May be false positive (reflection, dynamic dispatch) |
# Only show high-confidence and confirmed findings
searchdeadcode ./app --min-confidence high
# Only show runtime-confirmed findings (safest)
searchdeadcode ./app --coverage coverage.xml --runtime-only
Find code that passes static analysis but is never executed in practice:
# Include reachable but never-executed code
searchdeadcode ./app --coverage coverage.xml --include-runtime-dead
This detects "zombie code" - code that exists in your codebase and appears to be used (passes static analysis) but is never actually executed during test runs.
Leverage ProGuard/R8's usage.txt for confirmed dead code detection. R8 performs whole-program analysis during release builds and identifies code it will remove.
Add to your proguard-rules.pro:
-printusage usage.txt
Then build your release APK:
./gradlew assembleRelease
The file will be at: app/build/outputs/mapping/release/usage.txt
# Analyze with ProGuard data
searchdeadcode ./app --proguard-usage path/to/usage.txt
# Combine with other options
searchdeadcode ./app \
--proguard-usage usage.txt \
--coverage coverage.xml \
--detect-cycles
# Full analysis with R8 usage.txt
./target/release/searchdeadcode /path/to/your/android-project \
--exclude "**/build/**" \
--exclude "**/test/**" \
--exclude "**/Color.kt" \
--exclude "**/Theme.kt" \
--proguard-usage /path/to/your/android-project/app/usage.txt \
--detect-cycles
# Output:
# 📋 ProGuard usage.txt: 106329 unused items (24593 classes, 55479 methods)
# 🧟 Zombie Code Detected: 1 dead cycle (2 declarations)
# Found 21 dead code issues:
# ● 8 confirmed (matched with R8/ProGuard)
# ○ 13 medium confidence
📋 ProGuard usage.txt: 106329 unused items (24593 classes, 55479 methods)
Found 21 dead code issues:
Confidence Legend:
● Confirmed (runtime) ◉ High
○ Medium ◌ Low
/app/src/main/java/com/example/app/admin/ui/SingleLiveEvent.kt
● 22:1 warning [DC001] class 'SingleLiveEvent' is never used (confirmed by R8/ProGuard)
→ class 'SingleLiveEvent'
/base/src/main/java/com/example/common/text/HtmlFormatterHelper.kt
● 7:1 warning [DC001] class 'HtmlFormatterHelper' is never used (confirmed by R8/ProGuard)
→ class 'HtmlFormatterHelper'
────────────────────────────────────────────────────────────
Summary: 21 warnings
By Confidence:
● 8 confirmed (0 runtime-confirmed)
○ 13 medium confidence
| Benefit | Description |
|---|---|
| Confirmed findings | Items in usage.txt are marked as ● Confirmed |
| Cross-validation | Static analysis + R8 agreement = high confidence |
| Library dead code | R8 sees unused library code we can't analyze |
| False positive detection | const val objects may appear unused but are inlined |
const val inlining: Kotlin constants are inlined at compile time. The Events object may show as "unused" in usage.txt because only its values (not the object) are accessed at runtime. This is NOT dead code._Factory, _Impl, Dagger*, Hilt_* classes.Detect mutually dependent dead code - code that only references itself:
# Enable zombie code cycle detection
searchdeadcode ./app --detect-cycles
This finds patterns like:
Example output:
🧟 Zombie Code Detected:
2 dead cycles found (15 declarations)
Largest cycle: 8 mutually dependent declarations
3 zombie pairs (A↔B mutual references)
Cycle #1 (8 items):
• class 'LegacyHelper'
• class 'LegacyProcessor'
• method 'process'
• method 'handle'
... and 4 more
Detect function parameters that are declared but never used within the function body:
# Enable unused parameter detection
searchdeadcode ./app --unused-params
This detector is conservative to minimize false positives:
_unused) - Kotlin convention for intentionally unused paramsonXxx, *Listener, *Callback patternsDetect Android resources (strings, colors, dimensions, styles, etc.) that are defined but never referenced in code or XML:
# Enable unused resource detection
searchdeadcode ./app --unused-resources
Parses resource definitions from all res/values/*.xml files:
strings.xml → R.string.*colors.xml → R.color.*dimens.xml → R.dimen.*styles.xml → R.style.*attrs.xml → R.attr.*Scans for references in all Kotlin, Java, and XML files:
R.string.app_name, R.color.primary@string/app_name, @color/primaryReports unused resources with file location and resource type
$ searchdeadcode ./my-android-app --unused-resources
📦 Unused Android Resources:
○ app/src/main/res/values/strings.xml:21 - string 'unused_feature_text'
○ app/src/main/res/values/strings.xml:45 - string 'legacy_error_message'
○ app/src/main/res/values/colors.xml:12 - color 'deprecated_accent'
○ app/src/main/res/values/dimens.xml:8 - dimen 'old_margin_large'
○ app/src/main/res/values/styles.xml:15 - style 'LegacyButton'
○ base/src/main/res/values/attrs.xml:3 - attr 'customAttribute'
Found 6 unused resources (150 total defined, 320 referenced)
$ searchdeadcode /path/to/android-project --unused-resources
📦 Unused Android Resources:
○ app/src/main/res/values/admin_strings.xml:21 - string 'admin_apiMockAddressSaved'
○ app/src/main/res/values/appboy.xml:3 - string 'com_braze_api_key'
○ app/src/main/res/values/dimens.xml:8 - dimen 'card_sticky_audio_bottom_margin'
○ app/src/main/res/values/styles.xml:2 - style 'AppTheme.AppBarOverlay'
○ base/src/main/res/values/base_strings.xml:46 - string 'donation_button_text'
○ component-feed/src/main/res/values/feed_colors.xml:31 - color 'dates_light'
... and 47 more
Found 53 unused resources (672 total defined, 1142 referenced)
Some resources may appear unused but are actually required:
com_braze_*, google_*) - Read via reflectionUse --exclude patterns or add to your config file:
exclude:
- "**/appboy.xml"
- "**/google-services.xml"
For more aggressive dead code detection that analyzes individual members within classes:
# Enable deep analysis
searchdeadcode ./app --deep
Dead Code Analysis Results
==========================
com/example/app/utils/DeadHelper.kt
├─ class DeadHelper (line 5)
│ Never instantiated or referenced
└─ function unusedFunction (line 12)
Never called
com/example/app/models/LegacyModel.kt
└─ property debugFlag (line 8)
Assigned but never read
Summary: 3 issues found
- 1 unused class
- 1 unused function
- 1 assign-only property
{
"version": "1.1",
"total_issues": 21,
"issues": [
{
"code": "DC001",
"severity": "warning",
"confidence": "confirmed",
"confidence_score": 1.0,
"runtime_confirmed": true,
"message": "class 'DeadHelper' is never used (confirmed by R8/ProGuard)",
"file": "com/example/app/utils/DeadHelper.kt",
"line": 5,
"column": 1,
"declaration": {
"name": "DeadHelper",
"kind": "class",
"fully_qualified_name": "com.example.app.utils.DeadHelper"
}
}
],
"summary": {
"errors": 0,
"warnings": 21,
"infos": 0,
"by_confidence": {
"confirmed": 8,
"high": 0,
"medium": 13,
"low": 0
},
"runtime_confirmed_count": 8
}
}
| Field | Description |
|---|---|
code |
Issue code (DC001-DC007) |
confidence |
low, medium, high, confirmed |
confidence_score |
0.25 to 1.0 for sorting |
runtime_confirmed |
True if coverage data confirms unused |
fully_qualified_name |
Package path when available |
# Exclude patterns (glob syntax)
searchdeadcode ./app --exclude "**/test/**" --exclude "**/generated/**"
# Retain patterns (never report as dead)
searchdeadcode ./app --retain "*Activity" --retain "*ViewModel"
# Combine multiple filters
searchdeadcode ./app \
--exclude "**/build/**" \
--exclude "**/*Test.kt" \
--retain "*Repository" \
--retain "*UseCase"
# Interactive deletion (confirm each item)
searchdeadcode ./app --delete --interactive
# Batch deletion (select from list, confirm once)
searchdeadcode ./app --delete
# Dry run (preview only, no changes)
searchdeadcode ./app --delete --dry-run
# Generate undo script for recovery
searchdeadcode ./app --delete --undo-script restore.sh
Dry run - would delete:
class DeadHelper at com/example/utils/DeadHelper.kt:5
function unusedMethod at com/example/Service.kt:42
property debugFlag at com/example/Config.kt:8
Total: 3 items would be deleted
SearchDeadCode looks for configuration in these locations (in order):
--config flag.deadcode.yml / .deadcode.yaml in project root.deadcode.toml in project rootdeadcode.yml / deadcode.yaml / deadcode.toml in project root# .deadcode.yml
# Directories to analyze (relative to project root)
targets:
- "app/src/main/kotlin"
- "app/src/main/java"
- "feature/src/main/kotlin"
- "core/src/main/kotlin"
# Patterns to exclude from analysis (glob syntax)
exclude:
- "**/generated/**" # Generated code
- "**/build/**" # Build outputs
- "**/.gradle/**" # Gradle cache
- "**/.idea/**" # IDE files
- "**/test/**" # Test files (see note below)
- "**/*Test.kt" # Test classes
- "**/*Spec.kt" # Spec classes
# Patterns to retain - never report as dead (glob syntax)
# Use for code accessed via reflection, external libraries, etc.
retain_patterns:
- "*Adapter" # RecyclerView adapters
- "*ViewHolder" # ViewHolders
- "*Callback" # Callback interfaces
- "*Listener" # Event listeners
- "*Binding" # View bindings
# Explicit entry points (fully qualified class names)
entry_points:
- "com.example.app.MainActivity"
- "com.example.app.MyApplication"
- "com.example.api.PublicApi"
# Report configuration
report:
format: "terminal" # terminal | json | sarif
group_by: "file" # file | type | severity
show_code: true # Show code snippets in output
# Detection configuration - enable/disable specific detectors
detection:
unused_class: true # Unused classes and interfaces
unused_method: true # Unused methods and functions
unused_property: true # Unused properties and fields
unused_import: true # Unused import statements
unused_param: true # Unused function parameters
unused_enum_case: true # Unused enum values
assign_only: true # Write-only properties
dead_branch: true # Unreachable code branches
redundant_public: true # Public members only used internally
# Android-specific configuration
android:
parse_manifest: true # Parse AndroidManifest.xml for entry points
parse_layouts: true # Parse layout XMLs for class references
auto_retain_components: true # Auto-retain Android lifecycle components
component_patterns: # Additional patterns to auto-retain
- "*Activity"
- "*Fragment"
- "*Service"
- "*BroadcastReceiver"
- "*ContentProvider"
- "*ViewModel"
- "*Application"
- "*Worker" # WorkManager workers
# .deadcode.toml
targets = [
"app/src/main/kotlin",
"app/src/main/java",
]
exclude = [
"**/generated/**",
"**/build/**",
"**/test/**",
]
retain_patterns = [
"*Adapter",
"*ViewHolder",
]
entry_points = [
"com.example.app.MainActivity",
]
[report]
format = "terminal"
group_by = "file"
show_code = true
[detection]
unused_class = true
unused_method = true
unused_property = true
unused_import = true
unused_param = true
unused_enum_case = true
assign_only = true
dead_branch = true
redundant_public = true
[android]
parse_manifest = true
parse_layouts = true
auto_retain_components = true
component_patterns = [
"*Activity",
"*Fragment",
"*ViewModel",
]
searchdeadcode [OPTIONS] [PATH]
Arguments:
[PATH] Path to the project directory to analyze [default: .]
Options:
-c, --config <FILE> Path to configuration file
-t, --target <DIR> Target directories to analyze (can be repeated)
-e, --exclude <PATTERN> Patterns to exclude (can be repeated)
-r, --retain <PATTERN> Patterns to retain as entry points (can be repeated)
-f, --format <FORMAT> Output format [default: terminal]
[possible values: terminal, json, sarif]
-o, --output <FILE> Output file for json/sarif formats
--delete Enable safe delete mode
--interactive Interactive deletion (confirm each item)
--dry-run Preview deletions without making changes
--undo-script <FILE> Generate undo/restore script
--detect <TYPES> Detection types (comma-separated)
Analysis Options:
--deep Deep analysis mode - analyzes individual members
within classes for more aggressive detection
--unused-params Detect unused function parameters
--unused-resources Detect unused Android resources (strings, colors, etc.)
Hybrid Analysis Options:
--coverage <FILE> Coverage file (JaCoCo XML, Kover XML, or LCOV)
Can be specified multiple times for merged coverage
--proguard-usage <FILE> ProGuard/R8 usage.txt file for enhanced detection
--min-confidence Minimum confidence level to report
[possible values: low, medium, high, confirmed]
--runtime-only Only show findings confirmed by runtime coverage
--include-runtime-dead Include reachable but never-executed code
--detect-cycles Detect zombie code cycles (mutually dependent dead code)
-v, --verbose Verbose output
-q, --quiet Quiet mode - only output results
-h, --help Print help
-V, --version Print version
# Basic analysis
searchdeadcode /path/to/android/project
# Deep analysis (more aggressive, analyzes individual members)
searchdeadcode ./app --deep
# With exclusions
searchdeadcode ./app \
--exclude "**/build/**" \
--exclude "**/test/**" \
--exclude "**/generated/**"
# Full hybrid analysis (static + dynamic + R8)
searchdeadcode ./app \
--deep \
--coverage build/reports/jacoco.xml \
--proguard-usage app/build/outputs/mapping/release/usage.txt \
--detect-cycles \
--min-confidence high
# JSON output for CI/CD
searchdeadcode ./app \
--format json \
--output dead-code-report.json
# SARIF for GitHub Code Scanning
searchdeadcode ./app \
--format sarif \
--output results.sarif
# Safe delete with dry-run preview
searchdeadcode ./app --delete --dry-run
# Detect unused Android resources
searchdeadcode ./app --unused-resources
# Detect unused function parameters
searchdeadcode ./app --unused-params
# Full analysis with all enhanced detection
searchdeadcode ./app \
--deep \
--unused-params \
--unused-resources \
--detect-cycles
# Interactive deletion with undo script
searchdeadcode ./app \
--delete \
--interactive \
--undo-script restore.sh
# Only show confirmed dead code (highest confidence)
searchdeadcode ./app \
--coverage coverage.xml \
--proguard-usage usage.txt \
--runtime-only \
--min-confidence confirmed
Classes or interfaces that are never instantiated, extended, or referenced.
// DEAD: Never used anywhere
class OrphanHelper {
fun doSomething() {}
}
Methods that are never called, including extension functions.
class UserService {
fun getUser(id: String) = // used
// DEAD: Never called
fun legacyGetUser(id: Int) = // ...
}
// Extension functions are also detected
fun String.deadExtension(): String = this // DEAD: Never called
Properties declared but never read.
class Config {
val apiUrl = "https://api.example.com" // used
val debugMode = true // DEAD: never read
}
Properties that are written to but never read.
class Analytics {
var lastEventTime: Long = 0 // DEAD: assigned but never read
fun track(event: Event) {
lastEventTime = System.currentTimeMillis() // write-only
send(event)
}
}
Function parameters that are never used in the body.
// DEAD: 'context' parameter never used
fun formatDate(date: Date, context: Context): String {
return SimpleDateFormat("yyyy-MM-dd").format(date)
}
Import statements with no corresponding usage.
import com.example.utils.StringUtils // DEAD: never used
import com.example.models.User // used
class UserProfile {
fun display(user: User) {}
}
Individual enum values that are never referenced.
enum class Status {
ACTIVE, // used
INACTIVE, // used
LEGACY, // DEAD: never referenced
DEPRECATED // DEAD: never referenced
}
Public declarations only used within the same module.
// DEAD visibility: only used internally, could be internal/private
public class InternalHelper {
public fun process() {} // only called within this module
}
Code paths that can never be executed.
fun process(value: Int) {
if (value > 0) {
// reachable
} else if (value <= 0) {
// reachable
} else {
// DEAD: impossible to reach
handleImpossible()
}
}
The tool automatically retains (never reports as dead):
| Category | Patterns / Annotations |
|---|---|
| Lifecycle Components | *Activity, *Fragment, *Service, *BroadcastReceiver, *ContentProvider, *Application |
| Jetpack Compose | @Composable, @Preview |
| ViewModels | *ViewModel, @HiltViewModel |
| Dependency Injection | @Inject, @Provides, @Binds, @BindsOptionalOf, @BindsInstance, @IntoMap, @IntoSet, @Module, @Component, @HiltAndroidApp, @AndroidEntryPoint, @AssistedInject, @AssistedFactory |
| Serialization | @Serializable, @Parcelize, @JsonClass, @Entity, @SerializedName, @SerialName |
| Data Binding | @BindingAdapter, @InverseBindingAdapter, @BindingMethod, @BindingMethods, @BindingConversion |
| Room Database | @Dao, @Database, @Query, @Insert, @Update, @Delete, @RawQuery, @Transaction, @TypeConverter |
| Retrofit | @GET, @POST, @PUT, @DELETE, @PATCH, @HEAD, @OPTIONS, @HTTP, @Path, @Body, @Field, @Header |
| Testing | @Test, @Before, @After, @BeforeEach, @AfterEach, @BeforeAll, @AfterAll, @ParameterizedTest, @RunWith |
| Reflection | @JvmStatic, @JvmOverloads, @JvmField, @JvmName, @Keep |
| WorkManager | @HiltWorker |
| Lifecycle | @OnLifecycleEvent |
| Koin DI | @Factory, @Single, @KoinViewModel |
| Event Bus | @Subscribe |
| Coroutines | suspend functions (in reachable classes), @FlowPreview, @ExperimentalCoroutinesApi |
| Entry Functions | main() functions |
The tool parses Android XML files to detect additional entry points:
AndroidManifest.xml
<activity android:name=".MainActivity"><service android:name=".MyService"><receiver>, <provider>, <application> componentsLayout XMLs (res/layout/*.xml)
<com.example.CustomView>tools:context=".MyActivity"app:viewModel="@{viewModel}"Code that is only used in tests is considered dead code. This is intentional because:
To exclude test files from analysis:
exclude:
- "**/test/**"
- "**/androidTest/**"
- "**/*Test.kt"
- "**/*Spec.kt"
┌─────────────────────────────────────────────────────────────────┐
│ CLI Interface │
│ (clap + YAML config) │
└──────────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ File Discovery │
│ (ignore crate, respects .gitignore) │
│ .kt .java .xml files │
└──────────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Parsing Phase (Parallel) │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ tree-sitter- │ │ tree-sitter- │ │ quick-xml │ │
│ │ kotlin │ │ java │ │ (AndroidXML) │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└──────────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Declaration Registry │
│ │
│ HashMap<DeclarationId, Declaration> │
│ • Fully qualified names (com.example.MyClass.myMethod) │
│ • Location: file:line:column │
│ • Kind: class | method | property | function | enum | etc. │
└──────────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Reference Graph │
│ │
│ petgraph::DiGraph<Declaration, Reference> │
│ • Nodes = all declarations │
│ • Edges = usages/references between declarations │
└──────────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Entry Point Detection │
│ │
│ Android Roots (auto-retained): │
│ • Activity, Fragment, Service, BroadcastReceiver, Provider │
│ • @Composable functions │
│ • Classes in AndroidManifest.xml │
│ • Views referenced in layout XMLs │
│ • @Serializable, @Parcelize data classes │
│ • main() functions, @Test methods │
└──────────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Reachability Analysis │
│ │
│ DFS/BFS from entry points → mark reachable nodes │
│ Unreachable declarations = dead code candidates │
└──────────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Output │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ Terminal │ │ JSON │ │ SARIF │ │ Safe Delete │ │
│ │ (colored)│ │ (export) │ │ (CI) │ │ (interactive) │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
| Crate | Purpose | Why |
|---|---|---|
tree-sitter |
Core parsing | Incremental, error-tolerant parsing |
tree-sitter-kotlin (v0.3.6) |
Kotlin grammar | Official tree-sitter grammar |
tree-sitter-java (v0.21) |
Java grammar | Official tree-sitter grammar |
petgraph |
Graph data structure | Fast graph algorithms (DFS/BFS) |
ignore |
File discovery | Same as ripgrep, respects .gitignore |
rayon |
Parallelism | Parse files in parallel |
clap |
CLI parsing | Industry standard, derive macros |
serde |
Config parsing | YAML/TOML support |
quick-xml |
XML parsing | Fast AndroidManifest/layout parsing |
indicatif |
Progress bars | User feedback for large codebases |
colored |
Terminal colors | Readable output |
miette |
Error reporting | Beautiful diagnostics with code snippets |
dialoguer |
Interactive prompts | Safe delete confirmations |
searchdeadcode/
├── Cargo.toml
├── src/
│ ├── main.rs # CLI entry point
│ ├── lib.rs # Library exports
│ │
│ ├── config/
│ │ ├── mod.rs
│ │ └── loader.rs # YAML/TOML config loading
│ │
│ ├── discovery/
│ │ ├── mod.rs
│ │ └── file_finder.rs # Parallel file discovery
│ │
│ ├── parser/
│ │ ├── mod.rs
│ │ ├── kotlin.rs # Kotlin AST → declarations
│ │ ├── java.rs # Java AST → declarations
│ │ ├── xml/
│ │ │ ├── mod.rs
│ │ │ ├── manifest.rs # AndroidManifest.xml
│ │ │ └── layout.rs # Layout XMLs
│ │ └── common.rs # Shared types
│ │
│ ├── graph/
│ │ ├── mod.rs
│ │ ├── declaration.rs # Declaration types
│ │ ├── reference.rs # Reference types
│ │ └── builder.rs # Graph construction
│ │
│ ├── analysis/
│ │ ├── mod.rs
│ │ ├── entry_points.rs # Entry point detection
│ │ ├── reachability.rs # DFS/BFS traversal
│ │ └── detectors/
│ │ ├── mod.rs
│ │ ├── unused_class.rs
│ │ ├── unused_method.rs
│ │ ├── unused_property.rs
│ │ ├── unused_import.rs
│ │ ├── unused_param.rs
│ │ ├── unused_enum_case.rs
│ │ ├── assign_only.rs
│ │ ├── dead_branch.rs
│ │ └── redundant_public.rs
│ │
│ ├── refactor/
│ │ ├── mod.rs
│ │ ├── safe_delete.rs # Interactive deletion
│ │ ├── undo.rs # Restore script generation
│ │ └── editor.rs # File modification
│ │
│ └── report/
│ ├── mod.rs
│ ├── terminal.rs # Colored CLI output
│ ├── json.rs # JSON export
│ └── sarif.rs # SARIF for CI
│
├── tests/
│ ├── fixtures/
│ │ ├── kotlin/ # Test Kotlin files
│ │ ├── java/ # Test Java files
│ │ └── android/ # Full Android project
│ └── integration/
│
└── benches/
└── parsing_bench.rs # Performance benchmarks
All major features are implemented and tested:
Foo<T> → Foo)Reflection: Code accessed via reflection (e.g., Class.forName()) cannot be detected as used. Use retain_patterns for such cases.
Multi-module Projects: Each module is analyzed independently. Cross-module references work but require all modules to be in the analysis path.
Annotation Processors: Generated code (Dagger, Room, etc.) should be excluded as it may reference declarations in ways not visible to static analysis. However, the tool now properly recognizes most DI annotations (@Provides, @Binds, @Query, etc.) as entry points.
const val Inlining: Kotlin compile-time constants are inlined by the compiler. The tool now automatically skips const val properties to avoid false positives.
ProGuard Keep Rules: The tool doesn't parse ProGuard -keep rules. Use retain_patterns for kept classes, or verify against usage.txt output.
R. Resource References*: Android resource references (R.drawable.*, R.string.*, etc.) are compile-time constants and don't create trackable references in the code graph.
.gitignore or --exclude patterns.kt or .java filesIf code is incorrectly reported as dead:
entry_points in configretain_patterns for reflection/framework usage# Common false positive fixes
retain_patterns:
- "*Adapter" # RecyclerView adapters
- "*ViewHolder" # ViewHolders
- "*Callback" # Callback interfaces
- "*Binding" # Generated bindings
- "Dagger*" # Dagger components
<anonymous>This was fixed in v0.1.0. If you see this, ensure you're using the latest version.
Generic type references like Foo<Bar> now correctly match declarations Foo. This was fixed in v0.1.0.
Patterns like **/test/** now only match complete directory names, not substrings. /test/ matches, but /testproject/ does not.
Enhanced Detection (Phase 6)
--unused-resources flag: Detect unused Android resources (strings, colors, dimens, styles, attrs)
res/values/*.xml files for resource definitionsR.type.name and @type/name references--unused-params flag: Detect unused function parameters
Performance & CI Features (Phase 5)
--incremental flag: Incremental analysis with file caching
--watch flag: Watch mode for continuous monitoring
--baseline <FILE> flag: Baseline support for CI adoption
--generate-baseline <FILE>CLI Additions
--unused-resources - Detect unused Android resources--unused-params - Detect unused function parameters--incremental - Enable incremental analysis with caching--clear-cache - Clear the analysis cache--cache-path <FILE> - Custom cache file path--baseline <FILE> - Use baseline to filter existing issues--generate-baseline <FILE> - Generate baseline from current results--watch - Watch mode for continuous monitoringDeep Analysis Mode
--deep flag: More aggressive dead code detection that analyzes individual members within classesEnhanced DI/Framework Support
@Provides, @Binds, @Query, @GET, etc. are properly recognized as entry pointsKotlin Language Features
by lazy, by Delegates.observable(), etc.List<MyClass>, Map<K, V>, etc.class Foo : Bar by delegate patternsconst val properties (inlined at compile time)copy(), componentN(), equals(), hashCode(), toString()Results
New Features
--proguard-usage to load R8's usage.txt for confirmed dead code detection--detect-cycles--include-runtime-deadCLI Additions
--proguard-usage <FILE> - Load ProGuard/R8 usage.txt--coverage <FILE> - Load coverage data (can be repeated)--min-confidence <LEVEL> - Filter by confidence level--runtime-only - Only show runtime-confirmed findings--include-runtime-dead - Include reachable but never-executed code--detect-cycles - Enable zombie code cycle detectionOutput Improvements
Bug Fixes
<anonymous>)Focusable<T> now matches Focusable)obj.method() calls now detected)**/test/** no longer matches /testproject/)Improvements
Target performance goals (achieved):
| Codebase Size | Parse Time | Analysis Time |
|---|---|---|
| 1,000 files | < 1s | < 0.5s |
| 10,000 files | < 5s | < 2s |
| 100,000 files | < 30s | < 10s |
name: Dead Code Check
on: [push, pull_request]
jobs:
deadcode:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install SearchDeadCode
run: cargo install searchdeadcode
- name: Run Dead Code Analysis
run: searchdeadcode . --format sarif --output deadcode.sarif
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: deadcode.sarif
deadcode:
stage: analyze
script:
- cargo install searchdeadcode
- searchdeadcode . --format json --output deadcode.json
artifacts:
paths:
- deadcode.json
Contributions are welcome! Please:
cargo test)See AGENTS.md for the full contributor guide covering module layout, workflows, and review expectations.
This section documents the various paradigms and techniques used for dead code detection, based on research across industry tools and academic literature.
According to systematic literature reviews, there are two main approaches for automating dead code detection:
| Approach | Description | Tools |
|---|---|---|
| Accessibility Analysis | Build dependency graph, traverse from entry points, mark unreachable as dead | Periphery, SearchDeadCode, R8/ProGuard |
| Data Flow Analysis | Track how data flows through program, identify unused computations | Compilers (DCE), Static analyzers |
This is the approach used by SearchDeadCode, inspired by Periphery:
Entry Points → Build Dependency Graph → DFS/BFS Traversal → Mark Reachable → Report Unreachable
How Periphery works:
Key insight: The index store contains detailed information about declarations and their references, enabling accurate cross-file analysis.
Meta's SCARF system combines multiple analysis techniques:
Capabilities:
Impact at Meta:
Key technique: SCARF tracks two metrics - static usage (code that appears to use data) and runtime usage (actual access patterns in production).
Webpack and Rollup popularized tree shaking:
"Start with what you need, and work outwards" vs "Start with everything, and work backwards"
Algorithm:
Requirements:
import/export) - static structure requiredrequire) cannot be tree-shaken due to dynamic natureWebpack's implementation:
usedExports optimization marks unused exportsR8/ProGuard for Android:
Process:
R8 advantages over ProGuard:
Tools like ReSharper use solution-wide analysis:
Capabilities:
Key insight: Some dead code can only be detected at solution/project scope, not file scope.
Tools like deptry (Python) and Knip (TypeScript):
Detects:
Multi-module support:
From compiler theory (Wikipedia - Dead Code Elimination):
Data Flow Analysis:
Escape Analysis:
SSA-based DCE:
For large codebases, incremental analysis is essential:
Techniques:
Tools using incremental analysis:
| Paradigm | Accuracy | Speed | Scope | Best For |
|---|---|---|---|---|
| Graph Reachability | High | Fast | Project | General dead code |
| Static + Dynamic | Highest | Slow | Organization | Production code |
| Tree Shaking | High | Fast | Bundle | JavaScript modules |
| Compiler DCE | Highest | Build-time | Binary | Release builds |
| Scope Analysis | Medium | Real-time | IDE | Development feedback |
| Coverage-based | Medium | Requires runtime | Executed paths | Test coverage gaps |
Based on this research, potential enhancements include:
| Feature | Description | Inspiration | Status |
|---|---|---|---|
| Symbol-level analysis | Track individual variables, not just declarations | Meta SCARF | ✅ Done (v0.3.0 deep mode) |
| Cycle detection | Find mutually dependent dead code | Meta SCARF | ✅ Done (v0.2.0) |
| Coverage integration | Augment static analysis with runtime data | Hybrid tools | ✅ Done (v0.2.0) |
| Incremental mode | Cache results, only re-analyze changes | Glean, Roslyn | Planned |
| Transitive tracking | Track full reference chains | deptry, Knip | Partial |
| Cross-module analysis | Analyze multi-module projects holistically | Knip | Planned |
This section documents advanced dead code patterns beyond traditional "unreferenced code" detection. These patterns represent code that executes but serves no purpose - a more insidious form of technical debt.
Based on analysis of real-world Android codebases (1800+ files), we've prioritized these patterns by:
These patterns are common, easy to detect, and represent significant waste.
| # | Pattern | Detectability | Frequency | Description |
|---|---|---|---|---|
| 1 | Write-Only Variables | High | 58+ occurrences | Variables assigned but never read (private var x = 0 without reads) |
| 2 | Unused Sealed Class Variants | High | 73 sealed classes | Sealed class/interface cases that are never instantiated |
| 3 | Override Methods That Only Call Super | High | 284 overrides | override fun onCreate() { super.onCreate() } - adds no value |
| 4 | Ignored Return Values | High | Common | list.map { transform(it) } without using the result |
| 5 | Empty Catch Blocks | High | Common | catch (e: Exception) { } - swallowed errors |
| 6 | Unused Intent Extras | High | 90 putExtra calls | intent.putExtra("key", value) where "key" is never read |
| 7 | Write-Only SharedPreferences | High | Medium | prefs.edit().putString("x", y).apply() where "x" is never read |
| 8 | Write-Only Database Tables | High | 16 DAOs | @Insert without corresponding @Query usage |
| 9 | Redundant Null Checks | High | Common | user?.let { if (it != null) } - double null check |
| 10 | Dead Feature Flags | Medium | 388 isEnabled | if (RemoteConfig.isFeatureEnabled()) where flag is always true/false |
Detectable patterns with moderate frequency.
| # | Pattern | Detectability | Frequency | Description |
|---|---|---|---|---|
| 11 | Unobserved LiveData/StateFlow | Medium | 64 collectors | _state.value = x where _state is never observed in UI |
| 12 | Unused Constructor Parameters | High | Medium | Parameters passed to constructor but never used |
| 13 | Middle-Man Classes | Medium | Low | Classes that only delegate to other classes with no added logic |
| 14 | Lazy Classes | Medium | Low | Classes with minimal logic that could be inlined |
| 15 | Invariants Always True/False | High | Common | if (list.size >= 0) - always true |
| 16 | Cache Write Without Read | Medium | Medium | cache.save(data) but always fetching from network |
| 17 | Analytics Events Never Analyzed | Low | 253 log calls | Events tracked but no dashboard configured |
| 18 | Unused Type Parameters | High | Low | class Foo<T> where T is never used in the class |
| 19 | Dead Migrations | Medium | Low | Database migrations for versions no user has anymore |
| 20 | Listeners Never Triggered | Medium | Medium | view.setOnClickListener { } on views that can't be clicked |
High-value patterns that require more sophisticated analysis.
| # | Pattern | Detectability | Frequency | Description |
|---|---|---|---|---|
| 21 | Dormant Code Reactivated (Knight Capital Bug) | Low | Rare | Old code accidentally enabled by feature flags |
| 22 | Defensive Copies Never Modified | Medium | Low | val copy = list.toMutableList() but copy never mutated |
| 23 | Calculations Overwritten Immediately | Medium | Low | var x = expensiveCalc(); x = otherValue |
| 24 | Partially Dead Code | Medium | Medium | Code only used on some branches but computed on all |
| 25 | Recalculation of Available Values | Medium | Low | val h1 = data.hash(); ... val h2 = data.hash() |
| 26 | Audit Logs Never Queried | Low | Low | auditDao.insert(log) with no read methods |
| 27 | Breadcrumbs Without Consumer | Low | Low | Navigation history saved but never displayed |
| 28 | Event Bus Without Subscribers | Medium | Low | eventBus.post(event) with no @Subscribe for that event type |
| 29 | Coroutines Launched Then Cancelled | Low | Medium | Jobs cancelled before completing meaningful work |
| 30 | Workers That Produce Unused Output | Low | Low | WorkManager jobs whose results are never consumed |
Domain-specific or less common patterns.
| # | Pattern | Detectability | Frequency | Description |
|---|---|---|---|---|
| 31 | Annotations Without Effect | Medium | Low | @Keep when ProGuard isn't configured to use it |
| 32 | Validation After The Fact | Medium | Low | db.insert(x); require(x.isValid) - too late |
| 33 | Unused Debug Logging | High | 253 Timber calls | Logs in production that output to nowhere |
| 34 | Semi-Dead Classes | Medium | Low | Classes used as types but never instantiated |
| 35 | Test-Only Code in Production | High | Medium | Code only referenced by tests, never production |
| 36 | Timestamps Never Used | Medium | Low | updatedAt field maintained but never queried |
| 37 | Serializable Without Serialization | Medium | Low | @Serializable on classes never serialized |
| 38 | Crashlytics Keys Never Filtered | Low | Low | Custom keys set but never used in dashboard |
| 39 | Threads Spawned Without Work | Low | Rare | Executor pools with empty task queues |
| 40 | Configuration Values Never Read | Medium | Medium | Properties defined but never accessed |
Based on the priority analysis, here's the recommended implementation order:
Priority: ⭐⭐⭐⭐⭐
Patterns: #1, #7, #8, #26
Estimated dead code found: 15-25% increase
Detectors to implement:
WriteOnlyVariableDetector - Variables assigned but never readWriteOnlyPreferenceDetector - SharedPreferences written but never readWriteOnlyDatabaseDetector - DAO methods with @Insert but no @Query callersAlgorithm:
Priority: ⭐⭐⭐⭐
Patterns: #2, #3
Estimated dead code found: 10-15% increase
Detectors to implement:
UnusedSealedVariantDetector - Sealed subclasses never instantiatedRedundantOverrideDetector - Overrides that only call superAlgorithm for sealed variants:
Priority: ⭐⭐⭐
Patterns: #4, #6, #9
Estimated dead code found: 5-10% increase
Detectors to implement:
IgnoredReturnValueDetector - Function results not capturedUnusedIntentExtraDetector - putExtra without getExtraRedundantNullCheckDetector - Double null checksPriority: ⭐⭐
Patterns: #10, #11, #16
Estimated dead code found: 5-8% increase
Detectors to implement:
DeadFeatureFlagDetector - Flags always true/falseUnobservedStateDetector - StateFlow/LiveData never collectedWriteOnlyCacheDetector - Cache writes without readsPriority: ⭐
Patterns: #21-30
Estimated dead code found: 2-5% increase
Detectors to implement:
PartiallyDeadCodeDetector - Code used only on some pathsRecalculationDetector - Redundant recomputationEventBusOrphanDetector - Events without subscribersclass Analytics {
private var lastEventTime: Long = 0 // DEAD: never read
fun track(event: Event) {
lastEventTime = System.currentTimeMillis() // write-only
send(event)
}
}
sealed class UiState {
object Loading : UiState() // Used
data class Success(val data: Data) : UiState() // Used
data class Error(val msg: String) : UiState() // Used
object Empty : UiState() // DEAD: never emitted
}
override fun onCreateView(...): View {
return super.onCreateView(inflater, container, savedInstanceState)
// DEAD: If this is all it does, the override is unnecessary
}
@Dao
interface ReadHistoryDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun saveReadArticle(history: ReadHistory) // Called
@Query("SELECT * FROM read_history ORDER BY timestamp DESC")
fun getReadHistory(): Flow<List<ReadHistory>> // DEAD: never called!
}
// DEAD: The sorted list is never used
articles.sortedByDescending { it.date }
adapter.submitList(articles) // Still the original unsorted list!
// The flag has been true for 2 years
if (RemoteConfig.isNewPlayerEnabled()) { // Always true
playWithExoPlayer()
} else {
playWithMediaPlayer() // DEAD: never executed
}
From our analysis of a real-world Android project (1806 files):
| Pattern Category | Occurrences | Potential Dead Code |
|---|---|---|
| Timber/Log calls | 253 | ~50% may be production-silent |
| Override methods | 284 | ~10-20% may only call super |
| Intent extras (putExtra) | 90 | ~30% may be unread |
| Sealed classes | 73 | ~5-10% may have unused variants |
| Feature flags | 388 | ~20% may be dead branches |
| Flow collectors | 64 | ~10% may be unobserved |
| Map operations | 72 | ~5% may have ignored results |
| Private vars | 58 | ~20% may be write-only |
| DAO @Insert methods | 16 | ~10% may be write-only tables |
| DAO @Query methods | 49 | (Need cross-reference analysis) |
Estimated additional dead code: Using these advanced detectors could identify 30-50% more dead code beyond current detection.
Through thorough manual investigation of a real-world Android codebase, we verified the following concrete examples:
Example 1: feedStartUpdatingTimestamp in NewsToolbarController.kt:65
private var feedStartUpdatingTimestamp = 0L // Line 65
// Only written, never read:
feedStartUpdatingTimestamp = timeService.now().toInstant().toEpochMilli() // Line 102
File: feature-news/src/main/java/com/example/feed/news/toolbar/NewsToolbarController.kt
Example 2: Same pattern in ShowcaseToolbarController.kt:50
private var feedStartUpdatingTimestamp = 0L // Line 50
// Only written, never read:
feedStartUpdatingTimestamp = timeService.now().toInstant().toEpochMilli() // Line 124
File: feature-showcase/src/main/java/com/example/feed/showcase/ui/toolbar/ShowcaseToolbarController.kt
Impact: 2 confirmed write-only variables that store timestamps but never use them.
Found 20+ empty override methods that add no value:
| File | Line | Method |
|---|---|---|
ShowcaseToolbarController.kt |
137 | override fun onFragmentViewDestroyed() {} |
ListViewsFactory.kt |
30 | override fun onCreate() {} |
ListViewsFactory.kt |
46 | override fun onDestroy() {} |
StartupAdController.kt |
248 | override fun onActivityStarted(activity: Activity) {} |
StartupAdController.kt |
249 | override fun onActivityPaused(activity: Activity) {} |
StartupAdController.kt |
250 | override fun onActivityStopped(activity: Activity) {} |
StartupAdController.kt |
251 | override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} |
TimeViewHolder.kt |
76 | override fun unbind() {} |
MenuFeedDataSource.kt |
27 | override fun onAdapterViewBinded(position: Int) {} |
SingleScrollDirectionEnforcer.kt |
44 | override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) {} |
SingleScrollDirectionEnforcer.kt |
46 | override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {} |
MultipleCardFragment.kt |
139, 149, 151 | Empty animation listener methods |
Impact: These are interface requirements but represent code that does nothing.
During investigation, these patterns were verified as properly used (NOT dead code):
GlucheRepositoryImpl.getGluchePostStatus()BannerRepository.isDismissed()BackEndNotificationService.ktinit and read in scheduleVerifyIfServerHasNewPosts()This validates that our detection algorithm must follow the full call chain through repositories and services.
Based on the investigation, the Write-Only Variable detector must:
x = x + 1 counts x as read)by lazy, by BooleanPreferenceDelegate)The Empty Override detector must:
override fun declarationssuper.method()LifecycleObserver)Planned features and improvements for future releases:
--incremental)--watch)--unused-params)R.string.* usage with strings.xml (--unused-resources)--baseline)list.map{} without capturing resultWant to help? Here are good first issues:
entry_points.rsSee CONTRIBUTING.md for development setup and guidelines.
MIT