Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create CondFinalCondition readability check #935

Closed
wants to merge 8 commits into from
86 changes: 86 additions & 0 deletions lib/credo/check/readability/cond_final_condition.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
defmodule Credo.Check.Readability.CondFinalCondition do
use Credo.Check,
Stratus3D marked this conversation as resolved.
Show resolved Hide resolved
# Default to the value specified here:
# https://github.com/christopheradams/elixir_style_guide#true-as-last-condition
param_defaults: [final_condition_value: true],
explanations: [
check: """
If a cond expresion ends in an "always true" statement the statement
should be the literal `true`, or the literal value specified in this
check's `final_condition_value` parameter.

Example:

cond do
x == y -> 0
x > y -> 0
:else -> 1
end

# should be written as

cond do
x == y -> 0
x > y -> 0
true -> 1
end
""",
Stratus3D marked this conversation as resolved.
Show resolved Hide resolved
params: [
final_condition_value: "Set the expected value for the final condition"
Stratus3D marked this conversation as resolved.
Show resolved Hide resolved
]
]

@doc false
@impl true
def run(%SourceFile{} = source_file, params) do
issue_meta = IssueMeta.for(source_file, params)

final_condition_value = Params.get(params, :final_condition_value, __MODULE__)

Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta, final_condition_value))
end

defp traverse({:cond, meta, arguments} = ast, issues, issue_meta, final_condition_value) do
conditions =
arguments
|> Credo.Code.Block.do_block_for!()
|> List.wrap()

if conditions
|> List.last()
|> catchall_other_than_value?(final_condition_value) do
Stratus3D marked this conversation as resolved.
Show resolved Hide resolved
{ast, issues ++ [issue_for(issue_meta, meta[:line], :cond)]}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the trigger for the issue always "cond"? The trigger is the word in the code that "triggers" the issue and that should be the "wrong" final condition, e.g. using :else when using true is expected.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR updated. The trigger is now the literal catchall value, or in the case of maps, tuples, and integers, the atom :map, :tuple or :integer. Let me know if this is acceptable.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I mentioned before

The trigger is the word in the code that "triggers" the issue

which means the actual string that should get squiggly lines in your editor.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry about that. I don't know how to do that but I'll see if I can figure it out. Are there any other checks that I could refer to that do this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR updated. I have added assertions to the tests for this check to verify that the clause expression that triggers issue is what is set for the issue's trigger parameter.

else
{ast, issues}
end
end

defp traverse(ast, issues, _issue_meta, _min_pipeline_length) do
{ast, issues}
end

defp catchall_other_than_value?({:->, _meta, [[value], _args]}, value), do: false
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rename this function to something that gives a hint at what it does.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR updated. Let me know if the new name is acceptable.

# Integer literal catch-all clause
defp catchall_other_than_value?({:->, _meta, ['{', _args]}, _value), do: true
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this always return true, no matter what I specified in the config?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function returns false when the value variable bound by the match of the first argument ({:->, _meta, [[value], _args]}) matches the second argument value. The second argument passed to this function is value specified in the config.

# Binary literal catch-all clause
defp catchall_other_than_value?({:->, _meta, [[binary], _args]}, _value) when is_binary(binary), do: true
# List literal catch-all clause
defp catchall_other_than_value?({:->, _meta, [[list], _args]}, _value) when is_list(list), do: true
# Map literal catch-all clause
defp catchall_other_than_value?({:->, _meta, [[{:%{}, _meta2, []}], _args]}, _value), do: true
# Tuple literal catch-all clause
defp catchall_other_than_value?({:->, _meta, [[{:{}, _meta2, _values}], _args]}, _value), do: true
# Atom literal catch-all clause
defp catchall_other_than_value?({:->, _meta, [[name], _args]}, _value) when is_atom(name), do: true
defp catchall_other_than_value?(_clause, _value), do: false
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the clauses "above" this one are "catch alls" for all cases, what are examples where we need this clause?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for a non catch-all clause. A contrived example:

 cond do
   n > 5 ->
     "greater than 5"
   n >= 0 ->
     "positive"
   n < 0 ->
     "negative"
end

The last clause is not a catchall and is not composed of a single literal. I will add a comment for this final clause. Let me know if you think I should rename this function.


defp issue_for(issue_meta, line_no, trigger) do
format_issue(
issue_meta,
message:
"Cond statements that end with an \"always true\" condition should use `true` instead of some other literal.",
Stratus3D marked this conversation as resolved.
Show resolved Hide resolved
trigger: trigger,
line_no: line_no
)
end
end
222 changes: 222 additions & 0 deletions test/credo/check/readability/cond_final_condition_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
defmodule Credo.Check.Readability.CondFinalConditionTest do
use Credo.Test.Case

alias Credo.Check.Readability.CondFinalCondition
Stratus3D marked this conversation as resolved.
Show resolved Hide resolved

test "it should NOT report conds with a last condition of true" do
Stratus3D marked this conversation as resolved.
Show resolved Hide resolved
"""
defmodule CredoSampleModule do
def cond_true(a) do
cond do
a + 2 == 5 ->
"Nope"

a + 3 == 5 ->
"Uh, uh"

true ->
"OK"
end
end
end
"""
|> to_source_file()
|> run_check(CondFinalCondition)
|> refute_issues()
end

test "it should NOT report conds with a last condition that uses a variable" do
"""
defmodule CredoSampleModule do
def cond_true(a) do
cond do
a + 2 == 5 ->
"Nope"

a + 3 == 5 ->
"Uh, uh"
end
end
end
"""
|> to_source_file()
|> run_check(CondFinalCondition)
|> refute_issues()
end

test "it should report conds that with a last condition that is some other literal" do
"""
defmodule CredoSampleModule do
def cond_true(a) do
cond do
a + 2 == 5 ->
"Nope"

a + 3 == 5 ->
"Uh, uh"

:else ->
"OK"
end
end
end
"""
|> to_source_file()
|> run_check(CondFinalCondition)
|> assert_issue()
end

test "it should report conds that with a last condition that is binary literal" do
Stratus3D marked this conversation as resolved.
Show resolved Hide resolved
"""
defmodule CredoSampleModule do
def cond_true(a) do
cond do
a + 2 == 5 ->
"Nope"

a + 3 == 5 ->
"Uh, uh"

"else" ->
"OK"
end
end
end
"""
|> to_source_file()
|> run_check(CondFinalCondition)
|> assert_issue()
end

test "it should report conds that with a last condition that is integer literal" do
Stratus3D marked this conversation as resolved.
Show resolved Hide resolved
"""
defmodule CredoSampleModule do
def cond_true(a) do
cond do
a + 2 == 5 ->
"Nope"

a + 3 == 5 ->
"Uh, uh"

123 ->
"OK"
end
end
end
"""
|> to_source_file()
|> run_check(CondFinalCondition)
|> assert_issue()
end

test "it should report conds that with a last condition that is list literal" do
Stratus3D marked this conversation as resolved.
Show resolved Hide resolved
"""
defmodule CredoSampleModule do
def cond_true(a) do
cond do
a + 2 == 5 ->
"Nope"

a + 3 == 5 ->
"Uh, uh"

[:else] ->
"OK"
end
end
end
"""
|> to_source_file()
|> run_check(CondFinalCondition)
|> assert_issue()
end

test "it should report conds that with a last condition that is tuple literal" do
Stratus3D marked this conversation as resolved.
Show resolved Hide resolved
"""
defmodule CredoSampleModule do
def cond_true(a) do
cond do
a + 2 == 5 ->
"Nope"

a + 3 == 5 ->
"Uh, uh"

{:else} ->
"OK"
end
end
end
"""
|> to_source_file()
|> run_check(CondFinalCondition)
|> assert_issue()
end

test "it should report conds that with a last condition that is map literal" do
Stratus3D marked this conversation as resolved.
Show resolved Hide resolved
"""
defmodule CredoSampleModule do
def cond_true(a) do
cond do
a + 2 == 5 ->
"Nope"

a + 3 == 5 ->
"Uh, uh"

%{} ->
"OK"
end
end
end
"""
|> to_source_file()
|> run_check(CondFinalCondition)
|> assert_issue()
end

test "it should report conds that with a last condition that differ from config" do
Stratus3D marked this conversation as resolved.
Show resolved Hide resolved
"""
defmodule CredoSampleModule do
def cond_true(a) do
cond do
a + 2 == 5 ->
"Nope"

a + 3 == 5 ->
"Uh, uh"

true ->
"OK"
end
end
end
"""
|> to_source_file()
|> run_check(CondFinalCondition, final_condition_value: :else)
|> assert_issue()
end

test "it should NOT report conds that with a last condition that match config" do
Stratus3D marked this conversation as resolved.
Show resolved Hide resolved
"""
defmodule CredoSampleModule do
def cond_true(a) do
cond do
a + 2 == 5 ->
"Nope"

a + 3 == 5 ->
"Uh, uh"

:else ->
"OK"
end
end
end
"""
|> to_source_file()
|> run_check(CondFinalCondition, final_condition_value: :else)
|> refute_issues()
end
end