@@ -33,6 +33,7 @@ defmodule Credo.Check.Readability.NestedFunctionCalls do
3333 ]
3434 ]
3535
36+ alias Credo.Check.Readability.NestedFunctionCalls.PipeHelper
3637 alias Credo.Code.Name
3738
3839 @ doc false
@@ -42,55 +43,54 @@ defmodule Credo.Check.Readability.NestedFunctionCalls do
4243
4344 min_pipeline_length = Params . get ( params , :min_pipeline_length , __MODULE__ )
4445
45- { _continue , issues } =
46+ { _min_pipeline_length , issues } =
4647 Credo.Code . prewalk (
4748 source_file ,
48- & traverse ( & 1 , & 2 , issue_meta , min_pipeline_length ) ,
49- { true , [ ] }
49+ & traverse ( & 1 , & 2 , issue_meta ) ,
50+ { min_pipeline_length , [ ] }
5051 )
5152
5253 issues
5354 end
5455
55- # A call with no arguments
56- defp traverse ( { { :. , _loc , _call } , _meta , [ ] } = ast , { _ , issues } , _ , min_pipeline_length ) do
57- { ast , { min_pipeline_length , issues } }
56+ # A call in a pipeline
57+ defp traverse ( { :|> , _meta , [ pipe_input , { { :. , _meta2 , _fun } , _meta3 , args } ] } , acc , _issue ) do
58+ { [ pipe_input , args ] , acc }
59+ end
60+
61+ # A fully qualified call with no arguments
62+ defp traverse ( { { :. , _meta , _call } , _meta2 , [ ] } = ast , accumulator , _issue ) do
63+ { ast , accumulator }
5864 end
5965
60- # A call with arguments
66+ # Any call
6167 defp traverse (
62- { { :. , _loc , call } , meta , args } = ast ,
63- { _ , issues } ,
64- issue_meta ,
65- min_pipeline_length
68+ { { _name , _loc , call } , meta , args } = ast ,
69+ { min_pipeline_length , issues } = acc ,
70+ issue_meta
6671 ) do
67- if valid_chain_start ?( ast ) do
68- { ast , { min_pipeline_length , issues } }
72+ if cannot_be_in_pipeline ?( ast ) do
73+ { ast , acc }
6974 else
7075 case length_as_pipeline ( args ) + 1 do
7176 potential_pipeline_length when potential_pipeline_length >= min_pipeline_length ->
72- { ast ,
73- { min_pipeline_length , issues ++ [ issue_for ( issue_meta , meta [ :line ] , Name . full ( call ) ) ] } }
77+ new_issues = issues ++ [ issue_for ( issue_meta , meta [ :line ] , Name . full ( call ) ) ]
78+ { ast , { min_pipeline_length , new_issues } }
7479
7580 _ ->
76- { ast , { min_pipeline_length , issues } }
81+ { nil , acc }
7782 end
7883 end
7984 end
8085
81- # Another expression
82- defp traverse ( ast , { _ , issues } , _issue_meta , min_pipeline_length ) do
86+ # Another expression, we must no longer be in a pipeline
87+ defp traverse ( ast , { min_pipeline_length , issues } , _issue_meta ) do
8388 { ast , { min_pipeline_length , issues } }
8489 end
8590
86- # Call with no arguments
87- defp length_as_pipeline ( [ { { :. , _loc , _call } , _meta , [ ] } | _ ] ) do
88- 0
89- end
90-
9191 # Call with function call for first argument
92- defp length_as_pipeline ( [ { { :. , _loc , _call } , _meta , args } = call_ast | _ ] ) do
93- if valid_chain_start ?( call_ast ) do
92+ defp length_as_pipeline ( [ { _name , _meta , args } = call_ast | _ ] ) do
93+ if cannot_be_in_pipeline ?( call_ast ) do
9494 0
9595 else
9696 1 + length_as_pipeline ( args )
@@ -111,15 +111,247 @@ defmodule Credo.Check.Readability.NestedFunctionCalls do
111111 )
112112 end
113113
114- # Taken from the Credo.Check.Refactor.PipeChainStart module, with modifications
115- # map[:access]
116- defp valid_chain_start? ( { { :. , _ , [ Access , :get ] } , _ , _ } ) , do: true
114+ defp cannot_be_in_pipeline? ( ast ) do
115+ PipeHelper . cannot_be_in_pipeline? ( ast , [ ] , [ ] )
116+ end
117+
118+ defmodule PipeHelper do
119+ @ moduledoc """
120+ This module exists to contain logic for the cannot_be_in_pipline?/3 helper
121+ function. This function was originally copied from the
122+ Credo.Check.Refactor.PipeChainStart module's valid_chain_start?/3 function.
123+ Both functions are identical.
124+ """
125+
126+ @ elixir_custom_operators [
127+ :<- ,
128+ :||| ,
129+ :&&& ,
130+ :<<< ,
131+ :>>> ,
132+ :<<~ ,
133+ :~>> ,
134+ :<~ ,
135+ :~> ,
136+ :<~> ,
137+ :"<|>" ,
138+ :"^^^" ,
139+ :"~~~" ,
140+ :"..//"
141+ ]
142+
143+ def cannot_be_in_pipeline? (
144+ { :__block__ , _ , [ single_ast_node ] } ,
145+ excluded_functions ,
146+ excluded_argument_types
147+ ) do
148+ cannot_be_in_pipeline? (
149+ single_ast_node ,
150+ excluded_functions ,
151+ excluded_argument_types
152+ )
153+ end
154+
155+ for atom <- [
156+ :% ,
157+ :%{} ,
158+ :.. ,
159+ :<<>> ,
160+ :@ ,
161+ :__aliases__ ,
162+ :unquote ,
163+ :{} ,
164+ :& ,
165+ :<> ,
166+ :++ ,
167+ :-- ,
168+ :&& ,
169+ :|| ,
170+ :+ ,
171+ :- ,
172+ :* ,
173+ :/ ,
174+ :> ,
175+ :>= ,
176+ :< ,
177+ :<= ,
178+ :== ,
179+ :for ,
180+ :with ,
181+ :not ,
182+ :and ,
183+ :or
184+ ] do
185+ def cannot_be_in_pipeline? (
186+ { unquote ( atom ) , _meta , _arguments } ,
187+ _excluded_functions ,
188+ _excluded_argument_types
189+ ) do
190+ true
191+ end
192+ end
193+
194+ for operator <- @ elixir_custom_operators do
195+ def cannot_be_in_pipeline? (
196+ { unquote ( operator ) , _meta , _arguments } ,
197+ _excluded_functions ,
198+ _excluded_argument_types
199+ ) do
200+ true
201+ end
202+ end
203+
204+ # anonymous function
205+ def cannot_be_in_pipeline? (
206+ { :fn , _ , [ { :-> , _ , [ _args , _body ] } ] } ,
207+ _excluded_functions ,
208+ _excluded_argument_types
209+ ) do
210+ true
211+ end
212+
213+ # function_call()
214+ def cannot_be_in_pipeline? (
215+ { atom , _ , [ ] } ,
216+ _excluded_functions ,
217+ _excluded_argument_types
218+ )
219+ when is_atom ( atom ) do
220+ true
221+ end
222+
223+ # function_call(with, args) and sigils
224+ def cannot_be_in_pipeline? (
225+ { atom , _ , arguments } = ast ,
226+ excluded_functions ,
227+ excluded_argument_types
228+ )
229+ when is_atom ( atom ) and is_list ( arguments ) do
230+ sigil? ( atom ) ||
231+ valid_chain_start_function_call? (
232+ ast ,
233+ excluded_functions ,
234+ excluded_argument_types
235+ )
236+ end
237+
238+ # map[:access]
239+ def cannot_be_in_pipeline? (
240+ { { :. , _ , [ Access , :get ] } , _ , _ } ,
241+ _excluded_functions ,
242+ _excluded_argument_types
243+ ) do
244+ true
245+ end
246+
247+ # Module.function_call()
248+ def cannot_be_in_pipeline? (
249+ { { :. , _ , _ } , _ , [ ] } ,
250+ _excluded_functions ,
251+ _excluded_argument_types
252+ ) ,
253+ do: true
254+
255+ # Elixir <= 1.8.0
256+ # '__#{val}__' are compiled to String.to_charlist("__#{val}__")
257+ # we want to consider these charlists a valid pipe chain start
258+ def cannot_be_in_pipeline? (
259+ { { :. , _ , [ String , :to_charlist ] } , _ , [ { :<<>> , _ , _ } ] } ,
260+ _excluded_functions ,
261+ _excluded_argument_types
262+ ) ,
263+ do: true
264+
265+ # Elixir >= 1.8.0
266+ # '__#{val}__' are compiled to String.to_charlist("__#{val}__")
267+ # we want to consider these charlists a valid pipe chain start
268+ def cannot_be_in_pipeline? (
269+ { { :. , _ , [ List , :to_charlist ] } , _ , [ [ _ | _ ] ] } ,
270+ _excluded_functions ,
271+ _excluded_argument_types
272+ ) ,
273+ do: true
274+
275+ # Module.function_call(with, parameters)
276+ def cannot_be_in_pipeline? (
277+ { { :. , _ , _ } , _ , _ } = ast ,
278+ excluded_functions ,
279+ excluded_argument_types
280+ ) do
281+ valid_chain_start_function_call? (
282+ ast ,
283+ excluded_functions ,
284+ excluded_argument_types
285+ )
286+ end
287+
288+ def cannot_be_in_pipeline? ( _ , _excluded_functions , _excluded_argument_types ) , do: true
289+
290+ def valid_chain_start_function_call? (
291+ { _atom , _ , arguments } = ast ,
292+ excluded_functions ,
293+ excluded_argument_types
294+ ) do
295+ function_name = to_function_call_name ( ast )
296+
297+ found_argument_types =
298+ case arguments do
299+ [ nil | _ ] -> [ :atom ]
300+ x -> x |> List . first ( ) |> argument_type ( )
301+ end
302+
303+ Enum . member? ( excluded_functions , function_name ) ||
304+ Enum . any? (
305+ found_argument_types ,
306+ & Enum . member? ( excluded_argument_types , & 1 )
307+ )
308+ end
309+
310+ defp sigil? ( atom ) do
311+ atom
312+ |> to_string
313+ |> String . match? ( ~r/ ^sigil_[a-zA-Z]$/ )
314+ end
315+
316+ defp to_function_call_name ( { _ , _ , _ } = ast ) do
317+ { ast , [ ] , [ ] }
318+ |> Macro . to_string ( )
319+ |> String . replace ( ~r/ \. ?\( .*\) $/ s , "" )
320+ end
321+
322+ @ alphabet_wo_r ~w( a b c d e f g h i j k l m n o p q s t u v w x y z)
323+ @ all_sigil_chars Enum . flat_map ( @ alphabet_wo_r , & [ & 1 , String . upcase ( & 1 ) ] )
324+ @ matchable_sigils Enum . map ( @ all_sigil_chars , & :"sigil_#{ & 1 } " )
325+
326+ for sigil_atom <- @ matchable_sigils do
327+ defp argument_type ( { unquote ( sigil_atom ) , _ , _ } ) do
328+ [ unquote ( sigil_atom ) ]
329+ end
330+ end
117331
118- # Module.function_call()
119- defp valid_chain_start? ( { { :. , _ , _ } , _ , [ ] } ) , do: true
332+ defp argument_type ( { :sigil_r , _ , _ } ) , do: [ :sigil_r , :regex ]
333+ defp argument_type ( { :sigil_R , _ , _ } ) , do: [ :sigil_R , :regex ]
120334
121- # Kernel.to_string is invoked for string interpolation e.g. "string #{variable}"
122- defp valid_chain_start? ( { { :. , _ , [ Kernel , :to_string ] } , _ , _ } ) , do: true
335+ defp argument_type ( { :fn , _ , _ } ) , do: [ :fn ]
336+ defp argument_type ( { :%{} , _ , _ } ) , do: [ :map ]
337+ defp argument_type ( { :{} , _ , _ } ) , do: [ :tuple ]
338+ defp argument_type ( nil ) , do: [ ]
123339
124- defp valid_chain_start? ( _ ) , do: false
340+ defp argument_type ( v ) when is_atom ( v ) , do: [ :atom ]
341+ defp argument_type ( v ) when is_binary ( v ) , do: [ :binary ]
342+ defp argument_type ( v ) when is_bitstring ( v ) , do: [ :bitstring ]
343+ defp argument_type ( v ) when is_boolean ( v ) , do: [ :boolean ]
344+
345+ defp argument_type ( v ) when is_list ( v ) do
346+ if Keyword . keyword? ( v ) do
347+ [ :keyword , :list ]
348+ else
349+ [ :list ]
350+ end
351+ end
352+
353+ defp argument_type ( v ) when is_number ( v ) , do: [ :number ]
354+
355+ defp argument_type ( v ) , do: [ :credo_type_error , v ]
356+ end
125357end
0 commit comments