Code Review Best Practices
This skill activates when reviewing code for quality, correctness, security, and maintainability.
When to Use This Skill
Activate when:
-
Reviewing pull requests
-
Conducting code audits
-
Providing feedback on code quality
-
Identifying security vulnerabilities
-
Suggesting refactoring improvements
-
Checking adherence to coding standards
Code Review Checklist
- Correctness and Functionality
Does the code do what it's supposed to do?
-
Logic is correct and handles all cases
-
Edge cases are considered
-
Error handling is appropriate
-
No obvious bugs or logical errors
-
Assertions and validations are present
-
Return values are correct
Questions to ask:
-
What happens if this receives null/nil?
-
What if the list is empty?
-
What if the number is negative/zero?
-
Are there off-by-one errors?
-
Are comparisons correct (>, >=, <, <=)?
- Security
Is the code secure?
-
No SQL injection vulnerabilities
-
No XSS (Cross-Site Scripting) vulnerabilities
-
No CSRF vulnerabilities (CSRF protection in place)
-
User input is validated and sanitized
-
Sensitive data is not logged
-
Authentication and authorization are properly implemented
-
No hardcoded secrets or credentials
-
File uploads are validated (type, size, content)
-
External URLs are validated
-
Rate limiting is in place for APIs
Common security issues:
BAD: SQL injection vulnerability
query = "SELECT * FROM users WHERE id = #{user_id}"
GOOD: Use parameterized queries
query = from u in User, where: u.id == ^user_id
BAD: XSS vulnerability
raw("<div>#{user_input}</div>")
GOOD: Escape user input
<div><%= user_input %></div>
BAD: Hardcoded secrets
api_key = "sk_live_123456789"
GOOD: Use environment variables
api_key = System.get_env("API_KEY")
BAD: Mass assignment vulnerability
User.changeset(%User{}, params)
GOOD: Whitelist allowed fields
User.changeset(%User{}, params)
Where changeset only casts allowed fields:
cast(user, attrs, [:name, :email])
- Performance
Is the code efficient?
-
No N+1 query problems
-
Appropriate data structures chosen
-
Algorithms are efficient
-
Database indexes are used
-
Caching is implemented where appropriate
-
Large datasets are paginated or streamed
-
Unnecessary computations are avoided
-
Resources are cleaned up properly
Common performance issues:
BAD: N+1 query
posts = Repo.all(Post) Enum.map(posts, fn post -> author = Repo.get(User, post.author_id) # Query for each post! {post, author} end)
GOOD: Preload associations
posts = Post |> preload(:author) |> Repo.all()
BAD: Loading entire dataset
users = Repo.all(User) # Loads all millions of users Enum.filter(users, & &1.active)
GOOD: Query in database
users = User |> where(active: true) |> Repo.all()
BAD: Inefficient data structure
list = [1, 2, 3, 4, 5] if 3 in list do # O(n) lookup in list
...
end
GOOD: Use set/map for lookups
set = MapSet.new([1, 2, 3, 4, 5]) if MapSet.member?(set, 3) do # O(1) lookup
...
end
- Code Quality and Maintainability
Is the code readable and maintainable?
-
Clear, descriptive variable and function names
-
Functions are small and focused (single responsibility)
-
No code duplication (DRY principle)
-
Comments explain "why", not "what"
-
Code follows project conventions and style guide
-
Magic numbers are replaced with named constants
-
Complexity is minimized
-
Code is self-documenting
Code quality issues:
BAD: Unclear names
def calc(x, y, z) do r = x * y / z r * 1.2 end
GOOD: Clear names
def calculate_discounted_price(quantity, unit_price, discount_percentage) do subtotal = quantity * unit_price discount_amount = subtotal * (discount_percentage / 100) subtotal - discount_amount end
BAD: Long function with multiple responsibilities
def process_order(order) do
Validate order (responsibility 1)
Calculate totals (responsibility 2)
Update inventory (responsibility 3)
Send email (responsibility 4)
Log analytics (responsibility 5)
end
GOOD: Single responsibility functions
def process_order(order) do with {:ok, order} <- validate_order(order), {:ok, order} <- calculate_totals(order), {:ok, order} <- update_inventory(order), :ok <- send_confirmation_email(order), :ok <- log_order_analytics(order) do {:ok, order} end end
BAD: Magic numbers
if user.age >= 13 do
...
end
GOOD: Named constants
@minimum_age_coppa 13
if user.age >= @minimum_age_coppa do
...
end
- Error Handling
Are errors handled properly?
-
Errors don't crash the system unexpectedly
-
Error messages are helpful
-
Errors are logged appropriately
-
Happy path and error paths are both tested
-
No swallowed errors (empty catch blocks)
-
Proper error types are used
Error handling patterns:
BAD: Silent failure
try do dangerous_operation() rescue _ -> nil # Error is swallowed! end
GOOD: Handle errors explicitly
case dangerous_operation() do {:ok, result} -> result {:error, reason} -> Logger.error("Operation failed: #{inspect(reason)}") {:error, reason} end
BAD: Generic error message
{:error, "failed"}
GOOD: Specific error
{:error, :invalid_email_format} {:error, {:validation_failed, errors}}
BAD: Let it crash when shouldn't
def parse_config(path) do File.read!(path) # Crashes if file missing |> Jason.decode!() # Crashes if invalid JSON end
GOOD: Handle expected errors
def parse_config(path) do with {:ok, content} <- File.read(path), {:ok, config} <- Jason.decode(content) do {:ok, config} else {:error, :enoent} -> {:error, :config_file_not_found} {:error, %Jason.DecodeError{}} -> {:error, :invalid_config_format} end end
- Testing
Is the code properly tested?
-
New functionality has tests
-
Edge cases are tested
-
Error conditions are tested
-
Tests are clear and focused
-
Tests are deterministic (no flaky tests)
-
Test names describe what they test
-
Mocks are used appropriately
-
Test coverage is adequate
Testing concerns:
BAD: Unclear test name
test "test1" do
...
end
GOOD: Descriptive test name
test "create_user/1 returns error when email is invalid" do
...
end
BAD: Testing too much at once
test "user workflow" do
Creates user
Updates user
Deletes user
All in one test!
end
GOOD: Focused tests
test "create_user/1 creates user with valid attributes" do
...
end
test "update_user/2 updates user name" do
...
end
test "delete_user/1 removes user from database" do
...
end
BAD: Non-deterministic test
test "async operation completes" do start_async_operation() Process.sleep(100) # Race condition! assert operation_completed?() end
GOOD: Deterministic test
test "async operation completes" do start_async_operation() assert_receive {:completed, _result}, 1000 end
- Documentation
Is the code documented?
-
Public APIs have documentation
-
Complex logic has explanatory comments
-
README is updated if needed
-
Changelog is updated for user-facing changes
-
API documentation is accurate
-
Examples are provided
- Dependencies
Are dependencies handled properly?
-
New dependencies are justified
-
Dependencies are up-to-date and maintained
-
Licenses are compatible with project
-
Security vulnerabilities are checked
-
Dependency versions are pinned or bounded
Review Process
Before Reviewing
Understand the context
-
Read the PR description
-
Understand the problem being solved
-
Check related issues
Build and test locally
-
Pull the branch
-
Run tests
-
Test the functionality manually
During Review
Start with the big picture
-
Is the approach sound?
-
Does it fit the architecture?
-
Is there a better way?
Review for correctness
-
Does it work as intended?
-
Are edge cases handled?
-
Is error handling appropriate?
Check security and performance
-
Are there security vulnerabilities?
-
Will it perform well at scale?
Review code quality
-
Is it readable and maintainable?
-
Does it follow conventions?
-
Is it well-tested?
Providing Feedback
Be constructive and specific:
BAD: Vague criticism
"This function is bad."
GOOD: Specific, actionable feedback
"This function has three responsibilities: validation, database update, and email sending. Consider splitting it into separate functions for better testability and maintainability:
def update_user(user, attrs) do
with {:ok, changeset} <- validate_user_update(user, attrs),
{:ok, user} <- save_user(changeset),
:ok <- send_update_notification(user) do
{:ok, user}
end
end
BAD: Demanding
"You must change this."
GOOD: Collaborative
"What do you think about extracting this into a separate function? It would make the code easier to test."
BAD: Nitpicking without context
"Use single quotes instead of double quotes."
GOOD: Explain reasoning
"Our style guide prefers single quotes for consistency (see CONTRIBUTING.md section 3.2)."
**Use labels to categorize feedback:**
- **[blocking]**: Must be fixed before merging
- **[suggestion]**: Optional improvement
- **[question]**: Asking for clarification
- **[nit]**: Very minor, cosmetic issue
- **[security]**: Security concern
- **[performance]**: Performance concern
**Example:**
```markdown
[blocking] This creates a SQL injection vulnerability. Use parameterized queries:
```elixir
# Instead of:
query = "SELECT * FROM users WHERE name = '#{name}'"
# Use:
from(u in User, where: u.name == ^name)
[suggestion] Consider extracting this logic into a separate function for reusability.
[question] Why are we using a map here instead of a struct?
[nit] Extra blank line here.
### After Review
1. **Respond to author's questions**
2. **Re-review after changes**
3. **Approve when satisfied**
4. **Celebrate good code**
## Language-Specific Considerations
### Elixir
- Pattern matching is used effectively
- Functions leverage pipe operator for readability
- Atoms aren't created dynamically from untrusted input
- `with` statements handle errors properly
- Changesets validate all input
- No direct database queries in controllers/LiveViews (use contexts)
### JavaScript/TypeScript
- Types are properly defined (TypeScript)
- Promises are handled with .catch() or try/catch
- == vs === is used correctly
- Arrays/objects aren't mutated unexpectedly
- this binding is correct
- Async operations are properly awaited
### Python
- Type hints are used
- List comprehensions aren't overly complex
- Exceptions are specific (not bare except:)
- Resources are closed (use with statements)
- Code follows PEP 8
### Rust
- Ownership and borrowing are correct
- Error handling uses Result/Option properly
- Unsafe blocks are justified and minimal
- Clone/copy is used appropriately
- Lifetimes are correctly specified
## Common Code Smells
### Complexity Smells
- **Long functions** - Function does too much
- **Long parameter list** - Too many parameters
- **Deep nesting** - Too many levels of indentation
- **Complex conditionals** - Hard to understand if statements
### Duplication Smells
- **Copy-paste code** - Same code in multiple places
- **Similar functions** - Functions that do almost the same thing
- **Magic numbers** - Repeated literal values
### Naming Smells
- **Unclear names** - Variables like x, tmp, data
- **Misleading names** - Name doesn't match behavior
- **Inconsistent names** - Same concept called different things
### Design Smells
- **God object** - Class/module doing everything
- **Feature envy** - Function using another object's data more than its own
- **Inappropriate intimacy** - Too much coupling between modules
## Anti-Patterns to Watch For
### Premature Optimization
```elixir
# BAD: Optimizing before measuring
def calculate(data) do
# Complex, hard-to-read optimization
# that saves 0.1ms
end
# GOOD: Start simple, optimize if needed
def calculate(data) do
# Clear, simple code
# Optimize later if profiling shows bottleneck
end
Premature Abstraction
# BAD: Abstract after one use
defmodule AbstractDataProcessorFactoryBuilder do
# Complex abstraction for single use case
end
# GOOD: Wait for second use case
def process_user_data(data) do
# Simple, direct implementation
# Abstract when pattern emerges
end
Error Swallowing
# BAD: Hiding errors
try do
risky_operation()
rescue
_ -> :ok # What went wrong?
end
# GOOD: Handle explicitly
case risky_operation() do
{:ok, result} -> {:ok, result}
{:error, reason} ->
Logger.error("Operation failed: #{inspect(reason)}")
{:error, reason}
end
Review Etiquette
DO:
- Be respectful and constructive
- Assume good intent
- Ask questions instead of making demands
- Praise good code
- Explain the "why" behind suggestions
- Offer to pair program on complex issues
- Respond promptly to author's replies
DON'T:
- Be sarcastic or condescending
- Bike-shed on minor style issues
- Block on personal preferences
- Review your own code without another reviewer
- Approve code you don't understand
- Nitpick excessively
Self-Review Checklist
Before submitting code for review:
- Code compiles and runs
- All tests pass
- Added tests for new functionality
- No commented-out code
- No debug print statements
- Documentation is updated
- Commit messages are clear
- No secrets or sensitive data
- Code follows project style guide
- Changes are focused (no unrelated changes)
Key Principles
- Correctness first: Code must work correctly
- Security matters: Always consider security implications
- Be specific: Provide actionable, concrete feedback
- Be respectful: Kind, constructive communication
- Focus on important issues: Don't bike-shed
- Explain reasoning: Help author learn, don't just dictate
- Approve good code: Don't let perfect be enemy of good
- Collaborate: You're on the same team