parentheses_on_zero_arity_defs.ex (3195B)
1 defmodule Credo.Check.Readability.ParenthesesOnZeroArityDefs do 2 use Credo.Check, 3 base_priority: :low, 4 param_defaults: [parens: false], 5 explanations: [ 6 check: """ 7 Either use parentheses or not when defining a function with no arguments. 8 9 By default, this check enforces no parentheses, so zero-arity function 10 and macro definitions should look like this: 11 12 def summer? do 13 # ... 14 end 15 16 If the `:parens` param is set to `true` for this check, then the check 17 enforces zero-arity function and macro definitions to have parens: 18 19 def summer?() do 20 # ... 21 end 22 23 Like all `Readability` issues, this one is not a technical concern. 24 But you can improve the odds of others reading and liking your code by making 25 it easier to follow. 26 """ 27 ] 28 29 alias Credo.Check.Params 30 31 @def_ops [:def, :defp, :defmacro, :defmacrop] 32 33 @doc false 34 @impl true 35 def run(%SourceFile{} = source_file, params) do 36 parens? = Params.get(params, :parens, __MODULE__) 37 issue_meta = IssueMeta.for(source_file, params) 38 39 Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta, parens?)) 40 end 41 42 # TODO: consider for experimental check front-loader (ast) 43 for op <- @def_ops do 44 # catch variables named e.g. `defp` 45 defp traverse({unquote(op), _, nil} = ast, issues, _issue_meta, _parens?) do 46 {ast, issues} 47 end 48 49 defp traverse({unquote(op), _, body} = ast, issues, issue_meta, parens?) do 50 function_head = Enum.at(body, 0) 51 52 {ast, issues_for_definition(function_head, issues, issue_meta, parens?)} 53 end 54 end 55 56 defp traverse(ast, issues, _issue_meta, _parens?) do 57 {ast, issues} 58 end 59 60 # skip the false positive for a metaprogrammed definition 61 defp issues_for_definition({{:unquote, _, _}, _, _}, issues, _, _parens?) do 62 issues 63 end 64 65 defp issues_for_definition({_, _, args}, issues, _, _parens?) when length(args) > 0 do 66 issues 67 end 68 69 defp issues_for_definition({name, meta, _}, issues, issue_meta, enforce_parens?) do 70 line_no = meta[:line] 71 text = remaining_line_after(issue_meta, line_no, name) 72 parens? = String.match?(text, ~r/^\((\w*)\)(.)*/) 73 74 cond do 75 parens? and not enforce_parens? -> 76 issues ++ [issue_for(issue_meta, line_no, :present)] 77 78 not parens? and enforce_parens? -> 79 issues ++ [issue_for(issue_meta, line_no, :missing)] 80 81 true -> 82 issues 83 end 84 end 85 86 defp remaining_line_after(issue_meta, line_no, text) do 87 source_file = IssueMeta.source_file(issue_meta) 88 line = SourceFile.line_at(source_file, line_no) 89 name_size = text |> to_string |> String.length() 90 skip = (SourceFile.column(source_file, line_no, text) || -1) + name_size - 1 91 92 String.slice(line, skip..-1) 93 end 94 95 defp issue_for(issue_meta, line_no, kind) do 96 message = 97 case kind do 98 :present -> 99 "Do not use parentheses when defining a function which has no arguments." 100 101 :missing -> 102 "Use parentheses () when defining a function which has no arguments." 103 end 104 105 format_issue(issue_meta, message: message, line_no: line_no) 106 end 107 end