@@ -33,6 +33,7 @@ defmodule Credo.Check.Readability.NestedFunctionCalls do
33
33
]
34
34
]
35
35
36
+ alias Credo.Check.Readability.NestedFunctionCalls.PipeHelper
36
37
alias Credo.Code.Name
37
38
38
39
@ doc false
@@ -42,55 +43,54 @@ defmodule Credo.Check.Readability.NestedFunctionCalls do
42
43
43
44
min_pipeline_length = Params . get ( params , :min_pipeline_length , __MODULE__ )
44
45
45
- { _continue , issues } =
46
+ { _min_pipeline_length , issues } =
46
47
Credo.Code . prewalk (
47
48
source_file ,
48
- & traverse ( & 1 , & 2 , issue_meta , min_pipeline_length ) ,
49
- { true , [ ] }
49
+ & traverse ( & 1 , & 2 , issue_meta ) ,
50
+ { min_pipeline_length , [ ] }
50
51
)
51
52
52
53
issues
53
54
end
54
55
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 }
58
64
end
59
65
60
- # A call with arguments
66
+ # Any call
61
67
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
66
71
) 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 }
69
74
else
70
75
case length_as_pipeline ( args ) + 1 do
71
76
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 } }
74
79
75
80
_ ->
76
- { ast , { min_pipeline_length , issues } }
81
+ { nil , acc }
77
82
end
78
83
end
79
84
end
80
85
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
83
88
{ ast , { min_pipeline_length , issues } }
84
89
end
85
90
86
- # Call with no arguments
87
- defp length_as_pipeline ( [ { { :. , _loc , _call } , _meta , [ ] } | _ ] ) do
88
- 0
89
- end
90
-
91
91
# 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
94
94
0
95
95
else
96
96
1 + length_as_pipeline ( args )
@@ -111,15 +111,247 @@ defmodule Credo.Check.Readability.NestedFunctionCalls do
111
111
)
112
112
end
113
113
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
117
331
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 ]
120
334
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: [ ]
123
339
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
125
357
end
0 commit comments