-
Notifications
You must be signed in to change notification settings - Fork 434
Description
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
endWhile 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
endProposed 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
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 documentation
Questions for Maintainers
- ID Assignment: Is
EX5027the correct next ID for readability checks? - Anonymous functions: Should we flag directives in
fn -> alias Foo endor skip them? - Comprehensions: Should we check inside
forcomprehensions? - Default params: Are the proposed defaults reasonable?
- Naming: Does
StrictModuleDirectiveScopeclearly complementStrictModuleLayout?
Implementation Checklist
- Implement check with all configuration options
- Comprehensive test suite covering:
- All four directive types (alias, require, import, use)
- Nested control structures (if, case, with, cond, try)
- Multi-clause functions
- Quote block detection and skipping
- Test macro detection and skipping
- Private function handling
- Configuration parameter handling
- Edge cases (anonymous functions, comprehensions)
- Documentation with examples
- Update CHANGELOG.md
- Ensure check is disabled by default (controversial tag)
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.