Created
September 21, 2023 13:58
-
-
Save s3cur3/a83b361c40ae21fc00efd81c421cb4b0 to your computer and use it in GitHub Desktop.
A custom Credo check to disallow using the `unless` macro in Elixir
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# To use this check: | |
# 1. Add it to your project at `/lib/credo/disallow_unless.ex` | |
# 2. Add it to the list of `requires` in `.credo.exs`: | |
# | |
# requires: ["lib/credo/disallow_unless.ex"], # <-- add file here | |
# | |
# Add the module to the list of `checks` within `.credo.exs`: | |
# | |
# checks: [ | |
# {Credo.DisallowUnless}, # <-- add check here | |
# ] | |
# | |
# If you do not have a `.credo.exs` file yet, use `mix credo gen.config`. | |
defmodule Credo.DisallowUnless do | |
use Credo.Check, | |
base_priority: :high, | |
explanations: [ | |
check: """ | |
We consider `unless` to be unidiomatic. | |
Unlike languages like Swift or Ruby, where `unless` (or `guard`) can be used to | |
signal an early return from a function, Elixir doesn't have a concept of early | |
returns. Worse, `unless` can give new devs false confidence that it behaves like | |
a guard. Consider: | |
unless allowed? do | |
conn | |
|> put_status(:forbidden) | |
|> halt() | |
end | |
do_sensitive_thing() | |
In testing, you'll see that disallowed users correctly get a 403, but the | |
sensitive action is still performed! | |
""", | |
] | |
@doc false | |
@impl Credo.Check | |
def run(%SourceFile{} = source_file, params) do | |
issue_meta = IssueMeta.for(source_file, params) | |
Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta)) | |
end | |
defp traverse({:@, meta, [{:unless, _, _}]} = ast, issues, issue_meta) do | |
new_issue = issue_for(issue_meta, meta[:line], "unless") | |
{ast, issues ++ List.wrap(new_issue)} | |
end | |
defp traverse({:unless, meta, _arguments} = ast, issues, issue_meta) do | |
new_issue = issue_for(issue_meta, meta[:line], "unless") | |
{ast, issues ++ List.wrap(new_issue)} | |
end | |
defp traverse(ast, issues, _issue_meta), do: {ast, issues} | |
defp issue_for(issue_meta, line_no, trigger) do | |
format_issue( | |
issue_meta, | |
message: "Unless conditions are unidiomatic", | |
trigger: trigger, | |
line_no: line_no | |
) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment