New Check: StrictModuleDirectiveScope
Summary
Add a new readability check that enforces module directives (alias, require, import, use) are defined at the module level rather than inside function bodies.
Motivation
Module directives that appear inside function bodies can make code harder to follow and obscure module dependencies. By requiring all directives to be declared at the module level, readers can quickly understand a module's dependencies by looking at the top of the file.
Current Situation
Elixir allows module directives to be scoped to function bodies, which can lead to code like this:
defmodule MyModule do
def process_data(data) do
alias MyApp.DataProcessor
alias MyApp.Validator
require Logger
data
|> Validator.validate()
|> DataProcessor.process()
|> then(fn result ->
Logger.info("Processed: #{inspect(result)}")
result
end)
end
end
While this is valid Elixir, it has several drawbacks:
- Hidden dependencies: Module dependencies are not immediately visible
- Reduced scanability: Readers must examine each function to understand imports
- Inconsistency: Mixed patterns where some directives are at module level, others inline
- Refactoring friction: Moving code between functions requires moving directives
Proposed Improvement
The check would encourage this style instead:
defmodule MyModule do
alias MyApp.DataProcessor
alias MyApp.Validator
require Logger
def process_data(data) do
data
|> Validator.validate()
|> DataProcessor.process()
|> then(fn result ->
Logger.info("Processed: #{inspect(result)}")
result
end)
end
end
Proposed Check Details
Name
Credo.Check.Readability.StrictModuleDirectiveScope
Configuration
- ID:
EX5027
- Category:
:readability
- Base Priority:
:low
- Tags:
[:controversial] (disabled by default)
Parameters
param_defaults: [
# Which directives to check
directives: [:alias, :require, :import, :use],
# Allow directives in private functions (some teams prefer this)
allow_in_private_functions: false,
# Allow in test setup blocks (setup, describe, test macros)
allow_in_test_macros: true,
# Allow in quote blocks (macros that generate code for callers)
allow_in_quote_blocks: true,
# Function names to exclude from checking (regex patterns)
exclude_functions: []
]
Detection Scope
The check should detect directives in:
-
Function bodies (def, defp)
def foo do
alias Bar # ❌ Issue
end
-
Macro bodies (defmacro, defmacrop)
defmacro create_thing do
require Logger # ❌ Issue (unless in quote block)
end
-
Nested control structures
def foo(x) do
if x > 10 do
alias Bar # ❌ Issue
Bar.process(x)
end
end
-
Multi-clause functions
def foo(:ok) do
alias Success # ❌ Issue
Success.handle()
end
def foo(:error) do
alias Failure # ❌ Issue
Failure.handle()
end
-
Anonymous functions (controversial - could be excluded)
fn ->
alias Foo # ❌ Issue (maybe?)
Foo.bar()
end
Smart Skip Logic
The check should NOT flag directives in:
-
Quote blocks (macros generating code for callers)
defmacro my_macro do
quote do
alias MyModule # ✅ OK - generates code for caller
MyModule.do_something()
end
end
-
Test helper macros (when allow_in_test_macros: true)
setup do
alias MyApp.Factory # ✅ OK in test setup
{:ok, user: Factory.insert(:user)}
end
test "something" do
alias MyApp.Helper # ✅ OK in test macro
assert Helper.works?()
end
-
Private functions (when allow_in_private_functions: true)
defp helper do
alias Internal # ✅ OK if configured to allow
Internal.do_thing()
end
-
Module-level scope
defmodule MyModule do
alias Foo # ✅ OK - module level
def bar, do: Foo.baz()
end
Error Messages
"Alias Foo.Bar should be defined at module level, not inside function process_data/1"
"Import Logger should be defined at module level, not inside private function log_it/0"
"Require Protocol should be defined at module level, not inside function check/2"
"Use MyBehaviour should be defined at module level, not inside defmacro create/1"
Implementation Considerations
AST Traversal Strategy
The check needs to:
- Track context (are we inside a function/macro?)
- Detect all four directive types
- Handle nested structures (if, case, with, cond, try, rescue)
- Identify quote blocks to skip
- Identify test macros to skip (optional)
Controversy & Trade-offs
Why mark as controversial?
Some teams and use cases legitimately prefer inline directives:
- Namespace scoping: Limiting alias scope to where it's needed
- Phoenix LiveView components: Inline aliases in render functions
- Test organization: Grouping test-specific aliases with tests
- Macro hygiene: Keeping macro-internal dependencies localized
- Legacy codebases: Large refactoring burden
Counter-arguments for the check:
- Consistency: Encourages predictable module structure
- Discoverability: All dependencies visible at module top
- Tooling: Editor jump-to-definition works better
- Refactoring: Easier to move code between functions
- Code review: Dependencies clear in PR diffs
Compatibility with Other Checks
- StrictModuleLayout: Complementary - enforces order of module-level directives
- AliasUsage: Complementary - enforces when to alias vs. full module names
- UnusedOperation: May conflict if directives are conditionally used
Examples
Basic Usage
.credo.exs
%{
configs: [
%{
name: "default",
checks: [
# Disabled by default (controversial)
{Credo.Check.Readability.ModuleDirectiveScope, false}
]
}
]
}
Opt-in Configuration
{Credo.Check.Readability.StrictModuleDirectiveScope, []}
Relaxed Configuration (allow in private functions)
{Credo.Check.Readability.StrictModuleDirectiveScope, [
allow_in_private_functions: true
]}
Strict Configuration (check all directives, no exceptions)
{Credo.Check.Readability.StrictModuleDirectiveScope, [
directives: [:alias, :require, :import, :use],
allow_in_private_functions: false,
allow_in_test_macros: false,
allow_in_quote_blocks: true # Still allow in macros
]}
Exclude specific functions
{Credo.Check.Readability.StrictModuleDirectiveScope, [
exclude_functions: [~r/^render/, ~r/_test$/]
]}
Related Checks
Questions for Maintainers
- ID Assignment: Is
EX5027 the correct next ID for readability checks?
- Anonymous functions: Should we flag directives in
fn -> alias Foo end or skip them?
- Comprehensions: Should we check inside
for comprehensions?
- Default params: Are the proposed defaults reasonable?
- Naming: Does
StrictModuleDirectiveScope clearly complement StrictModuleLayout?
Implementation Checklist
Additional Context
This check was inspired by real-world experience maintaining a large Elixir codebase where inline directives made it difficult to understand module dependencies and refactor code. The goal is to provide teams who value this style with a tool to enforce it, while respecting that other teams may have valid reasons to use inline directives.
The controversial tag ensures this doesn't break existing codebases and allows teams to opt-in consciously.
New Check: StrictModuleDirectiveScope
Summary
Add a new readability check that enforces module directives (
alias,require,import,use) are defined at the module level rather than inside function bodies.Motivation
Module directives that appear inside function bodies can make code harder to follow and obscure module dependencies. By requiring all directives to be declared at the module level, readers can quickly understand a module's dependencies by looking at the top of the file.
Current Situation
Elixir allows module directives to be scoped to function bodies, which can lead to code like this:
While this is valid Elixir, it has several drawbacks:
Proposed Improvement
The check would encourage this style instead:
Proposed Check Details
Name
Credo.Check.Readability.StrictModuleDirectiveScopeConfiguration
EX5027:readability:low[:controversial](disabled by default)Parameters
Detection Scope
The check should detect directives in:
Function bodies (def, defp)
Macro bodies (defmacro, defmacrop)
Nested control structures
Multi-clause functions
Anonymous functions (controversial - could be excluded)
Smart Skip Logic
The check should NOT flag directives in:
Quote blocks (macros generating code for callers)
Test helper macros (when
allow_in_test_macros: true)Private functions (when
allow_in_private_functions: true)Module-level scope
Error Messages
Implementation Considerations
AST Traversal Strategy
The check needs to:
Controversy & Trade-offs
Why mark as controversial?
Some teams and use cases legitimately prefer inline directives:
Counter-arguments for the check:
Compatibility with Other Checks
Examples
Basic Usage
.credo.exs
Opt-in Configuration
Relaxed Configuration (allow in private functions)
Strict Configuration (check all directives, no exceptions)
Exclude specific functions
Related Checks
Credo.Check.Readability.StrictModuleLayout- Enforces order of module-level partsCredo.Check.Design.AliasUsage- Enforces when to use alias vs. full namesCredo.Check.Readability.ModuleDoc- Enforces module documentationQuestions for Maintainers
EX5027the correct next ID for readability checks?fn -> alias Foo endor skip them?forcomprehensions?StrictModuleDirectiveScopeclearly complementStrictModuleLayout?Implementation Checklist
Additional Context
This check was inspired by real-world experience maintaining a large Elixir codebase where inline directives made it difficult to understand module dependencies and refactor code. The goal is to provide teams who value this style with a tool to enforce it, while respecting that other teams may have valid reasons to use inline directives.
The controversial tag ensures this doesn't break existing codebases and allows teams to opt-in consciously.