defmodule Credo.Check.Refactor.LongQuoteBlocks do use Credo.Check, id: "EX4012", base_priority: :high, param_defaults: [max_line_count: 150, ignore_comments: false], explanations: [ check: """ Long `quote` blocks are generally an indication that too much is done inside them. Let's look at why this is problematic: defmodule MetaCommand do def __using__(opts \\\\ []) do modes = opts[:modes] command_name = opts[:command_name] quote do def run(filename) do contents = if File.exists?(filename) do {:ok, file} = File.open(filename, unquote(modes)) {:ok, contents} = IO.read(file, :line) File.close(file) contents else "" end case contents do "" -> # ... unquote(command_name) <> rest -> # ... end end # ... end end end A cleaner solution would be to call "regular" functions outside the `quote` block to perform the actual work. defmodule MyMetaCommand do def __using__(opts \\\\ []) do modes = opts[:modes] command_name = opts[:command_name] quote do def run(filename) do MyMetaCommand.run_on_file(filename, unquote(modes), unquote(command_name)) end # ... end end def run_on_file(filename, modes, command_name) do contents = # actual implementation end end This way it is easier to reason about what is actually happening. And to debug it. """, params: [ max_line_count: "The maximum number of lines a quote block should be allowed to have.", ignore_comments: "Ignores comments when counting the lines of a `quote` block." ] ] alias Credo.IssueMeta @doc false @impl true def run(%SourceFile{} = source_file, params) do issue_meta = IssueMeta.for(source_file, params) max_line_count = Params.get(params, :max_line_count, __MODULE__) ignore_comments = Params.get(params, :ignore_comments, __MODULE__) Credo.Code.prewalk( source_file, &traverse(&1, &2, issue_meta, max_line_count, ignore_comments) ) end defp traverse( {:quote, meta, arguments} = ast, issues, issue_meta, max_line_count, ignore_comments ) do max_line_no = Credo.Code.prewalk(arguments, &find_max_line_no(&1, &2), 0) line_count = max_line_no - meta[:line] issue = if line_count > max_line_count do source_file = IssueMeta.source_file(issue_meta) lines = source_file |> Credo.Code.to_lines() |> Enum.slice(meta[:line] - 1, line_count) lines = if ignore_comments do Enum.reject(lines, fn {_line_no, line} -> Regex.run(~r/^\s*#/, line) end) else lines end if Enum.count(lines) > max_line_count do issue_for(issue_meta, meta[:line]) end end {ast, issues ++ List.wrap(issue)} end defp traverse(ast, issues, _issue_meta, _max_line_count, _ignore_comments) do {ast, issues} end defp find_max_line_no({_, meta, _} = ast, max_line_no) do line_no = meta[:line] || 0 if line_no > max_line_no do {ast, line_no} else {ast, max_line_no} end end defp find_max_line_no(ast, max_line_no) do {ast, max_line_no} end defp issue_for(issue_meta, line_no) do format_issue( issue_meta, message: "Avoid long quote blocks.", trigger: "quote", line_no: line_no ) end end