diff --git a/.credo.exs b/.credo.exs index 23ae53c82..54190d55d 100644 --- a/.credo.exs +++ b/.credo.exs @@ -134,6 +134,7 @@ {Credo.Check.Refactor.RedundantWithClauseResult, []}, {Credo.Check.Refactor.RejectReject, []}, {Credo.Check.Refactor.UnlessWithElse, []}, + {Credo.Check.Refactor.UtcNowTruncate, []}, {Credo.Check.Refactor.WithClauses, []}, # diff --git a/lib/credo/check/refactor/utc_now_truncate.ex b/lib/credo/check/refactor/utc_now_truncate.ex new file mode 100644 index 000000000..66899a0fb --- /dev/null +++ b/lib/credo/check/refactor/utc_now_truncate.ex @@ -0,0 +1,186 @@ +defmodule Credo.Check.Refactor.UtcNowTruncate do + use Credo.Check, + id: "EX4032", + base_priority: :high, + explanations: [ + check: """ + `DateTime.utc_now/1` is more efficient than `DateTime.utc_now/0 |> DateTime.truncate/1`. + + For example, the code here ... + + DateTime.utc_now() |> DateTime.truncate(:second) + NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) + + ... can be refactored to look like this: + + DateTime.utc_now(:second) + NaiveDateTime.utc_now(:second) + + The reason for this is not just performance, because no separate function + call is required, but also brevity of the resulting code. + """ + ] + + @doc false + def run(source_file, params \\ []) do + issue_meta = IssueMeta.for(source_file, params) + + Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta)) + end + + # DateTime.truncate(DateTime.utc_now(), _) + # DateTime.truncate(DateTime.utc_now(_), _) + # DateTime.truncate(DateTime.utc_now(_, _), _) + defp traverse( + {{:., meta, [{:__aliases__, _, [:DateTime]}, :truncate]}, _, + [ + {{:., _, [{:__aliases__, _, [:DateTime]}, :utc_now]}, _, _}, + _ + ]} = + ast, + issues, + issue_meta + ) do + new_issue = issue_for(issue_meta, meta[:line], "DateTime") + {ast, issues ++ List.wrap(new_issue)} + end + + # DateTime.utc_now() |> DateTime.truncate(_) + # DateTime.utc_now(_) |> DateTime.truncate(_) + # DateTime.utc_now(_, _) |> DateTime.truncate(_) + defp traverse( + {:|>, _, + [ + {{:., _, [{:__aliases__, _, [:DateTime]}, :utc_now]}, _, _}, + {{:., meta, [{:__aliases__, _, [:DateTime]}, :truncate]}, _, [_]} + ]} = ast, + issues, + issue_meta + ) do + new_issue = issue_for(issue_meta, meta[:line], "DateTime") + {ast, issues ++ List.wrap(new_issue)} + end + + # DateTime.truncate(_ |> DateTime.utc_now(), _) + # DateTime.truncate(_ |> DateTime.utc_now(_), _) + defp traverse( + {{:., meta, [{:__aliases__, _, [:DateTime]}, :truncate]}, _, + [ + {:|>, _, + [ + _, + {{:., _, [{:__aliases__, _, [:DateTime]}, :utc_now]}, _, _} + ]}, + _ + ]} = ast, + issues, + issue_meta + ) do + new_issue = issue_for(issue_meta, meta[:line], "DateTime") + {ast, issues ++ List.wrap(new_issue)} + end + + # _ |> DateTime.utc_now() |> DateTime.truncate(_) + # _ |> DateTime.utc_now(_) |> DateTime.truncate(_) + defp traverse( + {:|>, _, + [ + {:|>, _, + [ + _, + {{:., _, [{:__aliases__, _, [:DateTime]}, :utc_now]}, _, _} + ]}, + {{:., meta, [{:__aliases__, _, [:DateTime]}, :truncate]}, _, [_]} + ]} = ast, + issues, + issue_meta + ) do + new_issue = issue_for(issue_meta, meta[:line], "DateTime") + {ast, issues ++ List.wrap(new_issue)} + end + + # NaiveDateTime.truncate(NaiveDateTime.utc_now(), _) + # NaiveDateTime.truncate(NaiveDateTime.utc_now(_), _) + # NaiveDateTime.truncate(NaiveDateTime.utc_now(_, _), _) + defp traverse( + {{:., meta, [{:__aliases__, _, [:NaiveDateTime]}, :truncate]}, _, + [ + {{:., _, [{:__aliases__, _, [:NaiveDateTime]}, :utc_now]}, _, _}, + _ + ]} = + ast, + issues, + issue_meta + ) do + new_issue = issue_for(issue_meta, meta[:line], "NaiveDateTime") + {ast, issues ++ List.wrap(new_issue)} + end + + # NaiveDateTime.utc_now() |> NaiveDateTime.truncate(_) + # NaiveDateTime.utc_now(_) |> NaiveDateTime.truncate(_) + # NaiveDateTime.utc_now(_, _) |> NaiveDateTime.truncate(_) + defp traverse( + {:|>, _, + [ + {{:., _, [{:__aliases__, _, [:NaiveDateTime]}, :utc_now]}, _, _}, + {{:., meta, [{:__aliases__, _, [:NaiveDateTime]}, :truncate]}, _, [_]} + ]} = ast, + issues, + issue_meta + ) do + new_issue = issue_for(issue_meta, meta[:line], "NaiveDateTime") + {ast, issues ++ List.wrap(new_issue)} + end + + # NaiveDateTime.truncate(_ |> NaiveDateTime.utc_now(), _) + # NaiveDateTime.truncate(_ |> NaiveDateTime.utc_now(_), _) + defp traverse( + {{:., meta, [{:__aliases__, _, [:NaiveDateTime]}, :truncate]}, _, + [ + {:|>, _, + [ + _, + {{:., _, [{:__aliases__, _, [:NaiveDateTime]}, :utc_now]}, _, _} + ]}, + _ + ]} = ast, + issues, + issue_meta + ) do + new_issue = issue_for(issue_meta, meta[:line], "NaiveDateTime") + {ast, issues ++ List.wrap(new_issue)} + end + + # _ |> NaiveDateTime.utc_now() |> NaiveDateTime.truncate(_) + # _ |> NaiveDateTime.utc_now(_) |> NaiveDateTime.truncate(_) + defp traverse( + {:|>, _, + [ + {:|>, _, + [ + _, + {{:., _, [{:__aliases__, _, [:NaiveDateTime]}, :utc_now]}, _, _} + ]}, + {{:., meta, [{:__aliases__, _, [:NaiveDateTime]}, :truncate]}, _, [_]} + ]} = ast, + issues, + issue_meta + ) do + new_issue = issue_for(issue_meta, meta[:line], "NaiveDateTime") + {ast, issues ++ List.wrap(new_issue)} + end + + defp traverse(ast, issues, _issue_meta) do + {ast, issues} + end + + defp issue_for(issue_meta, line_no, module) do + format_issue( + issue_meta, + message: + "Pass time unit to `#{module}.utc_now` instead of composing with `#{module}.truncate/2`.", + trigger: "#{module}.truncate", + line_no: line_no + ) + end +end diff --git a/test/credo/check/refactor/utc_now_truncate_test.exs b/test/credo/check/refactor/utc_now_truncate_test.exs new file mode 100644 index 000000000..82d49b5e1 --- /dev/null +++ b/test/credo/check/refactor/utc_now_truncate_test.exs @@ -0,0 +1,321 @@ +defmodule Credo.Check.Refactor.UtcNowTruncateTest do + use Credo.Test.Case + + @described_check Credo.Check.Refactor.UtcNowTruncate + + test "should report a violation when applying DateTime.truncate/2 to DateTime.utc_now/0" do + """ + defmodule M do + def f do + DateTime.truncate(DateTime.utc_now(), :second) + end + end + """ + |> to_source_file + |> run_check(@described_check) + |> assert_issue() + end + + test "should report a violation when applying DateTime.truncate/2 to DateTime.utc_now/1" do + """ + defmodule M do + def f do + DateTime.truncate(DateTime.utc_now(:second), :second) + end + end + """ + |> to_source_file + |> run_check(@described_check) + |> assert_issue() + end + + test "should report a violation when applying DateTime.truncate/2 to DateTime.utc_now/2" do + """ + defmodule M do + def f do + DateTime.truncate(DateTime.utc_now(Calendar.ISO, :second), :second) + end + end + """ + |> to_source_file + |> run_check(@described_check) + |> assert_issue() + end + + test "should report a violation when piping result of DateTime.utc_now/0 into DateTime.truncate/2" do + """ + defmodule M do + def f do + DateTime.utc_now() |> DateTime.truncate(:second) + end + end + """ + |> to_source_file + |> run_check(@described_check) + |> assert_issue() + end + + test "should report a violation when piping result of DateTime.utc_now/1 into DateTime.truncate/2" do + """ + defmodule M do + def f do + DateTime.utc_now(:second) |> DateTime.truncate(:second) + end + end + """ + |> to_source_file + |> run_check(@described_check) + |> assert_issue() + end + + test "should report a violation when piping result of DateTime.utc_now/2 into DateTime.truncate/2" do + """ + defmodule M do + def f do + DateTime.utc_now(Calendar.ISO, :second) |> DateTime.truncate(:second) + end + end + """ + |> to_source_file + |> run_check(@described_check) + |> assert_issue() + end + + test "should report a violation when piping argument to DateTime.utc_now/1 and applying DateTime.truncate/2 to that" do + """ + defmodule M do + def f do + DateTime.truncate(:second |> DateTime.utc_now(), :second) + end + end + """ + |> to_source_file + |> run_check(@described_check) + |> assert_issue() + end + + test "should report a violation when piping argument to DateTime.utc_now/2 and applying DateTime.truncate/2 to that" do + """ + defmodule M do + def f do + DateTime.truncate(Calendar.ISO |> DateTime.utc_now(:second), :second) + end + end + """ + |> to_source_file + |> run_check(@described_check) + |> assert_issue() + end + + test "should report a violation when piping argument to DateTime.utc_now/1 and piping result to DateTime.truncate/2" do + """ + defmodule M do + def f do + :second |> DateTime.utc_now() |> DateTime.truncate(:second) + end + end + """ + |> to_source_file + |> run_check(@described_check) + |> assert_issue() + end + + test "should report a violation when piping argument to DateTime.utc_now/2 and piping result to DateTime.truncate/2" do + """ + defmodule M do + def f do + Calendar.ISO |> DateTime.utc_now(:second) |> DateTime.truncate(:second) + end + end + """ + |> to_source_file + |> run_check(@described_check) + |> assert_issue() + end + + test "should report a violation when applying NaiveDateTime.truncate/2 to NaiveDateTime.utc_now/0" do + """ + defmodule M do + def f do + NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) + end + end + """ + |> to_source_file + |> run_check(@described_check) + |> assert_issue() + end + + test "should report a violation when applying NaiveDateTime.truncate/2 to NaiveDateTime.utc_now/1" do + """ + defmodule M do + def f do + NaiveDateTime.truncate(NaiveDateTime.utc_now(:second), :second) + end + end + """ + |> to_source_file + |> run_check(@described_check) + |> assert_issue() + end + + test "should report a violation when applying NaiveDateTime.truncate/2 to NaiveDateTime.utc_now/2" do + """ + defmodule M do + def f do + NaiveDateTime.truncate(NaiveDateTime.utc_now(Calendar.ISO, :second), :second) + end + end + """ + |> to_source_file + |> run_check(@described_check) + |> assert_issue() + end + + test "should report a violation when piping result of NaiveDateTime.utc_now/0 into NaiveDateTime.truncate/2" do + """ + defmodule M do + def f do + NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) + end + end + """ + |> to_source_file + |> run_check(@described_check) + |> assert_issue() + end + + test "should report a violation when piping result of NaiveDateTime.utc_now/1 into NaiveDateTime.truncate/2" do + """ + defmodule M do + def f do + NaiveDateTime.utc_now(:second) |> NaiveDateTime.truncate(:second) + end + end + """ + |> to_source_file + |> run_check(@described_check) + |> assert_issue() + end + + test "should report a violation when piping result of NaiveDateTime.utc_now/2 into NaiveDateTime.truncate/2" do + """ + defmodule M do + def f do + NaiveDateTime.utc_now(Calendar.ISO, :second) |> NaiveDateTime.truncate(:second) + end + end + """ + |> to_source_file + |> run_check(@described_check) + |> assert_issue() + end + + test "should report a violation when piping argument to NaiveDateTime.utc_now/1 and applying NaiveDateTime.truncate/2 to that" do + """ + defmodule M do + def f do + NaiveDateTime.truncate(:second |> NaiveDateTime.utc_now(), :second) + end + end + """ + |> to_source_file + |> run_check(@described_check) + |> assert_issue() + end + + test "should report a violation when piping argument to NaiveDateTime.utc_now/2 and applying NaiveDateTime.truncate/2 to that" do + """ + defmodule M do + def f do + NaiveDateTime.truncate(Calendar.ISO |> NaiveDateTime.utc_now(:second), :second) + end + end + """ + |> to_source_file + |> run_check(@described_check) + |> assert_issue() + end + + test "should report a violation when piping argument to NaiveDateTime.utc_now/1 and piping result to NaiveDateTime.truncate/2" do + """ + defmodule M do + def f do + :second |> NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) + end + end + """ + |> to_source_file + |> run_check(@described_check) + |> assert_issue() + end + + test "should report a violation when piping argument to NaiveDateTime.utc_now/2 and piping result to NaiveDateTime.truncate/2" do + """ + defmodule M do + def f do + Calendar.ISO |> NaiveDateTime.utc_now(:second) |> NaiveDateTime.truncate(:second) + end + end + """ + |> to_source_file + |> run_check(@described_check) + |> assert_issue() + end + + test "should report a violaton with a correct trigger value for DateTime.truncate/2" do + """ + defmodule M do + def f do + DateTime.truncate(DateTime.utc_now(), :second) + end + end + """ + |> to_source_file + |> run_check(@described_check) + |> assert_issue(fn issue -> assert issue.trigger == "DateTime.truncate" end) + end + + test "should report a violaton with a correct trigger value for NaiveDateTime.truncate/2" do + """ + defmodule M do + def f do + NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) + end + end + """ + |> to_source_file + |> run_check(@described_check) + |> assert_issue(fn issue -> assert issue.trigger == "NaiveDateTime.truncate" end) + end + + test "should report a violaton with a correct line_no value for DateTime.truncate/2" do + """ + defmodule M do + def f do + DateTime.utc_now() + |> + DateTime.truncate(:second) + end + end + """ + |> to_source_file + |> run_check(@described_check) + |> assert_issue(fn issue -> assert issue.line_no == 5 end) + end + + test "should report a violaton with a correct line_no value for NaiveDateTime.truncate/2" do + """ + defmodule M do + def f do + NaiveDateTime.utc_now() + |> + NaiveDateTime.truncate(:second) + end + end + """ + |> to_source_file + |> run_check(@described_check) + |> assert_issue(fn issue -> assert issue.line_no == 5 end) + end +end