match_in_condition.ex (3829B)
1 defmodule Credo.Check.Refactor.MatchInCondition do 2 use Credo.Check, 3 param_defaults: [ 4 allow_tagged_tuples: false 5 ], 6 explanations: [ 7 check: """ 8 Pattern matching should only ever be used for simple assignments 9 inside `if` and `unless` clauses. 10 11 While this fine: 12 13 # okay, simple wildcard assignment: 14 15 if contents = File.read!("foo.txt") do 16 do_something(contents) 17 end 18 19 the following should be avoided, since it mixes a pattern match with a 20 condition and do/else blocks. 21 22 # considered too "complex": 23 24 if {:ok, contents} = File.read("foo.txt") do 25 do_something(contents) 26 end 27 28 # also considered "complex": 29 30 if allowed? && ( contents = File.read!("foo.txt") ) do 31 do_something(contents) 32 end 33 34 If you want to match for something and execute another block otherwise, 35 consider using a `case` statement: 36 37 case File.read("foo.txt") do 38 {:ok, contents} -> 39 do_something() 40 _ -> 41 do_something_else() 42 end 43 44 """, 45 params: [ 46 allow_tagged_tuples: 47 "Allow tagged tuples in conditions, e.g. `if {:ok, contents} = File.read( \"foo.txt\") do`" 48 ] 49 ] 50 51 @condition_ops [:if, :unless] 52 @trigger "=" 53 54 @doc false 55 @impl true 56 def run(%SourceFile{} = source_file, params) do 57 issue_meta = IssueMeta.for(source_file, params) 58 allow_tagged_tuples = Params.get(params, :allow_tagged_tuples, __MODULE__) 59 60 Credo.Code.prewalk(source_file, &traverse(&1, &2, allow_tagged_tuples, issue_meta)) 61 end 62 63 # Skip if arguments is not enumerable 64 defp traverse({_op, _meta, nil} = ast, issues, _allow_tagged_tuples, _source_file) do 65 {ast, issues} 66 end 67 68 # TODO: consider for experimental check front-loader (ast) 69 # NOTE: we have to exclude the cases matching the above 70 for op <- @condition_ops do 71 defp traverse({unquote(op), _meta, arguments} = ast, issues, allow_tagged_tuples, issue_meta) do 72 # remove do/else blocks 73 condition_arguments = Enum.reject(arguments, &Keyword.keyword?/1) 74 75 new_issues = 76 Credo.Code.prewalk( 77 condition_arguments, 78 &traverse_condition( 79 &1, 80 &2, 81 unquote(op), 82 condition_arguments, 83 allow_tagged_tuples, 84 issue_meta 85 ) 86 ) 87 88 {ast, issues ++ new_issues} 89 end 90 end 91 92 defp traverse(ast, issues, _allow_tagged_tuples, _source_file) do 93 {ast, issues} 94 end 95 96 defp traverse_condition( 97 {:=, meta, arguments} = ast, 98 issues, 99 op, 100 op_arguments, 101 allow_tagged_tuples?, 102 issue_meta 103 ) do 104 assignment_in_body? = Enum.member?(op_arguments, ast) 105 106 case arguments do 107 [{atom, _, nil}, _right] when is_atom(atom) -> 108 if assignment_in_body? do 109 {ast, issues} 110 else 111 new_issue = issue_for(op, meta[:line], issue_meta) 112 113 {ast, issues ++ [new_issue]} 114 end 115 116 [{tag_atom, {atom, _, nil}}, _right] when is_atom(atom) and is_atom(tag_atom) -> 117 if allow_tagged_tuples? do 118 {ast, issues} 119 else 120 new_issue = issue_for(op, meta[:line], issue_meta) 121 122 {ast, issues ++ [new_issue]} 123 end 124 125 _ -> 126 new_issue = issue_for(op, meta[:line], issue_meta) 127 {ast, issues ++ [new_issue]} 128 end 129 end 130 131 defp traverse_condition(ast, issues, _op, _op_arguments, _allow_tagged_tuples, _issue_meta) do 132 {ast, issues} 133 end 134 135 defp issue_for(op, line_no, issue_meta) do 136 format_issue( 137 issue_meta, 138 message: "There should be no matches in `#{op}` conditions.", 139 trigger: @trigger, 140 line_no: line_no 141 ) 142 end 143 end