Detect Unused Public Functions in Elixir with This Simple Script
Overview
As your Elixir codebase grows, it's common to accumulate unused public functions that were once needed but are no longer called anywhere in your application. These orphaned functions create technical debt, making your code harder to navigate and maintain. This guide presents a simple yet powerful bash script that helps you identify these unused functions so you can clean up your codebase with confidence.
The Problem with Unused Functions
Dead code in your codebase has several negative impacts:
- Cognitive overhead: Developers waste time reading and understanding functions that aren't actually used
- Maintenance burden: Unused functions still need to be updated during refactoring or dependency upgrades
- False dependencies: They may import or use modules that could otherwise be removed
- Test complexity: You might be maintaining tests for functions that no longer serve a purpose
- Compilation time: More code means longer compile times
The Solution: A Simple Bash Script
While Elixir has excellent tooling, it doesn't include a built-in way to detect unused public functions. This bash script fills that gap by using standard Unix tools to analyze your codebase.
The Script
#!/bin/bash
# Find unused public functions using only Unix commands
# Usage: ./find_unused_functions.sh
echo "=========================================="
echo "UNUSED PUBLIC FUNCTIONS"
echo "Generated: $(date)"
echo "=========================================="
echo ""
# Find all public function definitions and check if unused
grep -rn --include='*.ex' '^[[:space:]]*def [a-z_][a-z0-9_?!]*[( ]' lib/ | \
grep -v 'defp\|defmodule\|defmacro\|defdelegate\|defstruct\|defimpl\|defprotocol\|defexception' | \
sed -E 's/^(.+):([0-9]+):[[:space:]]*def ([a-z_][a-z0-9_?!]*).*/\1:\2:\3/' | \
while IFS=':' read -r file line func; do
# Escape regex special characters in function name (? and !)
escaped_func=$(echo "$func" | sed 's/[?!]/\\&/g')
# Search for usages:
# - .func( or .func/ : module.function() calls or captures
# - &func/ : function captures
# - :func : atoms (apply, etc.)
# - func( or func do : calls with parens or definitions without parens
# - <.func : Phoenix component syntax
pattern="\\.${escaped_func}[(/]|&${escaped_func}/|:${escaped_func}\\b|\\b${escaped_func}[( ]|<\\.${escaped_func}\\b"
count=$(grep -rEc --include='*.ex' --include='*.exs' --include='*.eex' --include='*.heex' --include='*.leex' "$pattern" lib test 2>/dev/null | \
grep -E ':[1-9][0-9]*' | \
cut -d: -f2 | \
awk '{sum+=$1} END {print sum+0}')
# Only output if unused (count <= 1)
if [ "$count" -le 1 ]; then
echo "$func (defined in $file:$line)"
fi
done > /tmp/unused_functions.txt
# Print results
cat /tmp/unused_functions.txt
echo ""
echo "=========================================="
echo "Found $(wc -l < /tmp/unused_functions.txt) unused function(s)"
echo "=========================================="
How the Script Works
Let's break down what each part of the script does:
1. Finding Public Function Definitions
grep -rn --include='*.ex' '^[[:space:]]*def [a-z_][a-z0-9_?!]*[( ]' lib/
This searches for lines that start with def followed by a function name. It only looks in *.ex files within the lib/ directory.
2. Filtering Out Non-Function Definitions
grep -v 'defp\|defmodule\|defmacro\|defdelegate\|defstruct\|defimpl\|defprotocol\|defexception'
This excludes private functions (defp) and other def* keywords that aren't public function definitions.
3. Extracting Function Information
sed -E 's/^(.+):([0-9]+):[[:space:]]*def ([a-z_][a-z0-9_?!]*).*/\1:\2:\3/'
This extracts three pieces of information: the file path, line number, and function name.
4. Searching for Function Usages
The script searches for various patterns that indicate a function is being used:
.func(or.func/- Module-qualified calls likeMyModule.my_function()or function captures&func/- Function captures like&my_function/2:func- Atom references used withapply/3or similarfunc(orfunc do- Direct calls within the same module<.func- Phoenix LiveView component syntax
5. Counting References
The script counts how many times each function is referenced across all .ex, .exs, .eex, .heex, and .leex files in both lib/ and test/ directories.
6. Identifying Unused Functions
if [ "$count" -le 1 ]; then
echo "$func (defined in $file:$line)"
fi
A function is considered unused if it appears only once (its own definition). If the count is 1 or less, the function is likely unused.
Using the Script
Step 1: Save the Script
Create a file named find_unused_functions.sh in your project root and paste the script above.
Step 2: Make it Executable
chmod +x find_unused_functions.sh
Step 3: Run the Script
./find_unused_functions.sh
Example Output
==========================================
UNUSED PUBLIC FUNCTIONS
Generated: Mon Mar 10 14:32:15 PST 2026
==========================================
calculate_discount (defined in lib/shop/pricing.ex:42)
send_notification (defined in lib/users/notifier.ex:18)
format_address (defined in lib/utils/formatter.ex:67)
==========================================
Found 3 unused function(s)
==========================================
Important Considerations
False Positives
The script may report functions as unused in certain scenarios:
- Public API functions: Functions intended to be called by external applications or libraries
- Behaviour callbacks: Functions implementing behaviour callbacks may not be directly called
- Dynamic calls: Functions invoked using
apply/3,Kernel.apply/2, or string-based module resolution - Mix tasks: Functions called from custom Mix tasks
- Configuration callbacks: Functions referenced in config files
- Test helpers: Functions only used in test files but defined in
lib/
Manual Review Required
Always review the results manually before deleting functions. Consider:
- Is this function part of a public API?
- Could this function be used dynamically?
- Is this a callback for a behaviour or protocol?
- Should this function be private instead of deleted?
Best Practices
1. Run Regularly
Include this script in your regular code review process. Consider running it:
- Before major refactoring sessions
- As part of quarterly code cleanup sprints
- When removing or deprecating features
2. Make Functions Private When Possible
If a function is only used within its module, convert it from def to defp:
# Before
def internal_helper(data) do
# ...
end
# After
defp internal_helper(data) do
# ...
end
3. Document Public APIs
Add @doc annotations to functions that are intentionally public, making it clear they're part of your API:
@doc """
Calculates the total price including tax.
This function is part of the public API and may be called by external systems.
"""
def calculate_total(subtotal, tax_rate) do
# ...
end
4. Use Behaviours for Callbacks
Define behaviours to make it explicit which functions are callbacks:
defmodule MyApp.PaymentProcessor do
@callback process_payment(amount :: float()) :: {:ok, String.t()} | {:error, String.t()}
end
Extending the Script
Exclude Specific Patterns
You can modify the script to exclude certain patterns. For example, to skip functions ending with _callback:
grep -rn --include='*.ex' '^[[:space:]]*def [a-z_][a-z0-9_?!]*[( ]' lib/ | \
grep -v 'defp\|defmodule\|_callback'
Save to a File
Redirect output to a file for easier review:
./find_unused_functions.sh > unused_functions_report.txt
Integrate with CI/CD
Add the script to your CI pipeline to track unused functions over time:
# .github/workflows/code-quality.yml
- name: Check for unused functions
run: |
./find_unused_functions.sh
if [ $(wc -l < /tmp/unused_functions.txt) -gt 10 ]; then
echo "Warning: More than 10 unused functions detected"
fi
Alternative Tools
While this script works well for most projects, you might also consider:
- Credo: A static code analysis tool that can detect some unused code patterns
- Dialyzer: While primarily for type checking, it can sometimes identify unreachable code
- Custom Mix tasks: Build a more sophisticated analyzer using Elixir's AST parsing
Conclusion
Maintaining a clean codebase is essential for long-term project health. This simple bash script provides a quick and effective way to identify unused public functions in your Elixir projects. By regularly running this script and cleaning up dead code, you'll improve code readability, reduce maintenance burden, and keep your project lean and focused.
Remember that automated tools are helpers, not replacements for human judgment. Always review the results carefully and consider the context before removing functions. Some functions may appear unused but serve important purposes in your architecture.