(formerly SearchDeadCode)
Find and eliminate dead code in Android projects
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.
See it in action: analyze an Android project in seconds
- Features
- Comparison with Alternatives
- Installation
- Quick Start
- CLI Reference
- Configuration
- Detection Types
- When NOT to Use Chazer
- Troubleshooting
- Contributing
| 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) |
- Interactive mode: Confirm each deletion individually
- Batch mode: Review all candidates, confirm once
- Dry-run: Preview what would be deleted
- Undo support: Generate restore scripts
How does Chazer compare to other tools?
| Feature | Chazer | Android Lint | R8/ProGuard | Detekt | IntelliJ |
|---|---|---|---|---|---|
| Speed | β‘ <1s/1k files | π’ Slow | π¨ Build-time | π’ Medium | π’ Medium |
| Kotlin-first | β Native | β Yes | β Yes | β Yes | |
| Java support | β Yes | β Yes | β Yes | β No | β Yes |
| Safe delete | β Interactive | β No | β No | β No | β IDE only |
| CI/CD ready | β SARIF, JSON | β XML | β No | β SARIF | β No |
| Coverage integration | β JaCoCo, Kover | β No | β No | β No | β No |
| Cycle detection | β Zombie code | β No | β No | β No | β No |
| Resource detection | β Yes | β Yes | β No | β No | β Yes |
| Config file | β YAML/TOML | β XML | β ProGuard | β YAML | β No |
| Standalone | β No build needed | β Gradle | β Build | β Gradle | β IDE |
| Open source | β MIT | β Apache | β Proprietary | β Apache | β Proprietary |
| Tool | Best For |
|---|---|
| Chazer | Fast CI checks, pre-commit hooks, quick project audits, Kotlin-first projects |
| Android Lint | Comprehensive Android-specific checks beyond dead code |
| R8/ProGuard | Production builds with 100% accuracy (but no interactive deletion) |
| Detekt | Kotlin code style and complexity analysis |
| IntelliJ | Interactive development with refactoring support |
Pro tip: Use Chazer for fast feedback during development, then validate with R8's usage.txt before major
cleanups.
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 |
brew tap dr7ro0t/tap
brew install chazerDownload the latest release from GitHub Releases.
Available binaries:
chazer-linux-x86_64- Linux (Intel/AMD 64-bit)chazer-linux-aarch64- Linux (ARM 64-bit)chazer-macos-x86_64- macOS (Intel)chazer-macos-aarch64- macOS (Apple Silicon)chazer-windows-x86_64.exe- Windows (64-bit)
macOS may show a security warning because the binary isn't code-signed. To run it:
Option 1: Remove quarantine attribute (recommended)
xattr -d com.apple.quarantine ~/Downloads/chazer-macos-*
chmod +x ~/Downloads/chazer-macos-*Option 2: Right-click β Open
- Right-click the binary in Finder
- Select "Open" from the context menu
- Click "Open" in the dialog
Option 3: System Preferences
- Go to System Preferences β Privacy & Security
- Click "Open Anyway" next to the blocked app message
git clone https://github.com/dr7ro0t/Chazer
cd Chazer
cargo install --path .# Analyze your Android project
chazer ./my-android-app
# Preview what would be deleted
chazer ./my-android-app --delete --dry-run$ chazer ./my-app --min-confidence high
Chazer v0.5.1
Discovering files...
Found 247 files to analyze
Parsing files...
Detecting entry points...
Found 89 entry points
Running reachability analysis...
Reachability: 1,847 reachable, 2,103 total
Found 12 dead code issues:
Confidence Legend:
β Confirmed (runtime) β High β Medium β Low
app/src/main/java/com/example/data/OldApiClient.kt
β 15:1 warning [DC001] class 'LegacyApiClient' is never used
β class 'LegacyApiClient'
app/src/main/java/com/example/utils/StringUtils.kt
β 42:5 warning [DC001] function 'formatLegacyDate' is never used
β function 'formatLegacyDate'
β 67:5 warning [DC001] function 'parseOldFormat' is never used
β function 'parseOldFormat'
app/src/main/java/com/example/models/User.kt
β 23:5 warning [DC004] property 'middleName' is never used
β property 'middleName'
Summary: 12 issues in 4 files (3 classes, 5 functions, 4 properties)
Estimated removable lines: ~340
chazer [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
chazer /path/to/android/project
# Deep analysis (more aggressive, analyzes individual members)
chazer ./app --deep
# With exclusions
chazer ./app \
--exclude "**/build/**" \
--exclude "**/test/**" \
--exclude "**/generated/**"
# Full hybrid analysis (static + dynamic + R8)
chazer ./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
chazer ./app \
--format json \
--output dead-code-report.json
# SARIF for GitHub Code Scanning
chazer ./app \
--format sarif \
--output results.sarif
# Safe delete with dry-run preview
chazer ./app --delete --dry-run
# Detect unused Android resources
chazer ./app --unused-resources
# Detect unused function parameters
chazer ./app --unused-params
# Full analysis with all enhanced detection
chazer ./app \
--deep \
--unused-params \
--unused-resources \
--detect-cycles
# Interactive deletion with undo script
chazer ./app \
--delete \
--interactive \
--undo-script restore.sh
# Only show confirmed dead code (highest confidence)
chazer ./app \
--coverage coverage.xml \
--proguard-usage usage.txt \
--runtime-only \
--min-confidence confirmedGenerate tab completions for your shell:
# Bash
chazer --completions bash > ~/.local/share/bash-completion/completions/chazer
# Zsh
chazer --completions zsh > ~/.zfunc/_chazer
# Fish
chazer --completions fish > ~/.config/fish/completions/chazer.fishYou can easily add dead code detection to your CI pipeline
# .github/workflows/dead-code.yml
name: Dead Code Detection
on: [ push, pull_request ]
jobs:
dead-code:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Detect Dead Code
uses: dr7ro0t/Chazer@v0
with:
path: '.'
fail-on-findings: 'true' # Fail CI on dead code
min-confidence: 'medium'
format: 'sarif' # SARIF output for GitHub Security
output: 'dead-code.sarif'
args: '--deep --unused-params --write-only --sealed-variants' # Deep analysis with all detectors| Input | Description | Default |
|---|---|---|
path |
Path to analyze | . |
version |
Chazer version | latest |
format |
Output format: terminal, json, sarif |
terminal |
output |
Output file path | - |
args |
Additional CLI arguments | - |
fail-on-findings |
Fail if dead code found | false |
min-confidence |
Minimum confidence: low, medium, high, confirmed |
medium |
deadcode:
stage: analyze
script:
- cargo install chazer
- chazer . --format json --output deadcode.json
artifacts:
paths:
- deadcode.jsonChazer looks for configuration in these locations (in order):
- Path specified via
--configflag .deadcode.yml/.deadcode.yamlin project root.deadcode.tomlin project rootdeadcode.yml/deadcode.yaml/deadcode.tomlin 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",
]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 calledProperties 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>components
Layout XMLs (res/layout/*.xml)
- Custom views:
<com.example.CustomView> - Context references:
tools:context=".MyActivity" - Data binding:
app:viewModel="@{viewModel}"
Code that is only used in tests is considered dead code. This is intentional because:
- Test-only utilities should be in test directories
- Production code shouldn't exist solely for testing
- Such code adds maintenance burden without production value
To exclude test files from analysis:
exclude:
- "**/test/**"
- "**/androidTest/**"
- "**/*Test.kt"
- "**/*Spec.kt"Being honest about limitations helps you choose the right tool. Don't use Chazer if:
Static analysis cannot catch everything. If you need guaranteed accuracy:
- Use R8/ProGuard's
usage.txtoutput (generated during release builds) - Chazer can validate against
usage.txtwith--proguard-usage
If your codebase relies heavily on reflection:
// We can't detect this as "used"
Class.forName("com.example.MyClass").newInstance()Workaround: Add reflection targets to retain_patterns in your config.
Chazer is Kotlin-first. While Java is supported, it shines on:
- Kotlin projects
- Mixed Kotlin/Java Android projects
For pure Java, consider UCDetector or IntelliJ's built-in inspections.
Chazer is a CLI tool. If you prefer IDE integration:
- Use IntelliJ/Android Studio's built-in "Unused declaration" inspection
- Or run Chazer in watch mode alongside your IDE
We analyze JVM bytecode patterns. JavaScript or other dynamic targets aren't supported.
- Speed: Analyze 10k files in seconds, not minutes
- CI Integration: Block PRs that add dead code
- Safe Deletion: Interactive mode with undo scripts
- Coverage Integration: Combine static + dynamic analysis
- No Build Required: Analyze without compiling
-
Reflection: Code accessed via reflection (e.g.,
Class.forName()) cannot be detected as used. Useretain_patternsfor 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 valInlining: Kotlin compile-time constants are inlined by the compiler. The tool now automatically skipsconst valproperties to avoid false positives. -
ProGuard Keep Rules: The tool doesn't parse ProGuard
-keeprules. Useretain_patternsfor 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.
- Check that your target path is correct
- Ensure files aren't excluded by
.gitignoreor--excludepatterns - Verify the project structure has
.ktor.javafiles
If code is incorrectly reported as dead:
- Check entry points: Add to
entry_pointsin config - Check patterns: Add to
retain_patternsfor reflection/framework usage - Check annotations: Ensure framework annotations are recognized
- Check XML: Verify AndroidManifest.xml and layouts are being parsed
# Common false positive fixes
retain_patterns:
- "*Adapter" # RecyclerView adapters
- "*ViewHolder" # ViewHolders
- "*Callback" # Callback interfaces
- "*Binding" # Generated bindings
- "Dagger*" # Dagger componentsThis 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.
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Write tests for new functionality
- Ensure all tests pass (
cargo test) - Submit a pull request
See AGENTS.md for the full contributor guide covering module layout, workflows, and review expectations.
- Periphery - Swift dead code detector (architecture inspiration)
- tree-sitter - Incremental parsing library
- ripgrep - Fast file search (ignore crate)
- ast-grep - Structural code search
- rust-code-analysis - Mozilla's code analysis library