@@ -22,6 +22,16 @@ defmodule Mix.Tasks.Format do
22
22
* `:inputs` (a list of paths and patterns) - specifies the default inputs
23
23
to be used by this task. For example, `["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]`.
24
24
25
+ * `:subdirectories` (a list of paths and patterns) - specifies subdirectories
26
+ that have their own formatting rules. Each subdirectory should have a
27
+ `.formatter.exs` that configures how entries in that subdirectory should be
28
+ formatted as. Configuration between `.formatter.exs` are not shared nor
29
+ inherited. If a `.formatter.exs` lists "lib/app" as a subdirectory, the rules
30
+ in `.formatter.exs` won't be available in the inner `lib/app/.formatter.exs`.
31
+ It is also important that the parent `.formatter.exs` does not specify files
32
+ inside the "lib/app" subdirectory in its `:inputs` configuration. If this
33
+ happens, the behaviour of which formatter will be picked is unspecified.
34
+
25
35
* `:import_deps` (a list of dependencies as atoms) - specifies a list
26
36
of dependencies whose formatter configuration will be imported.
27
37
When specified, the formatter should run in the same directory as
@@ -115,16 +125,19 @@ defmodule Mix.Tasks.Format do
115
125
dry_run: :boolean
116
126
]
117
127
118
- @ deps_manifest "cached_formatter_deps"
128
+ @ manifest "cached_dot_formatter"
129
+ @ manifest_vsn 1
119
130
120
131
def run ( args ) do
121
132
{ opts , args } = OptionParser . parse! ( args , strict: @ switches )
122
133
{ dot_formatter , formatter_opts } = eval_dot_formatter ( opts )
123
- formatter_opts = fetch_deps_opts ( dot_formatter , formatter_opts )
134
+
135
+ { formatter_opts_and_subs , _sources } =
136
+ eval_deps_and_subdirectories ( dot_formatter , [ ] , formatter_opts , [ dot_formatter ] )
124
137
125
138
args
126
- |> expand_args ( formatter_opts )
127
- |> Task . async_stream ( & format_file ( & 1 , opts , formatter_opts ) , ordered: false , timeout: 30000 )
139
+ |> expand_args ( dot_formatter , formatter_opts_and_subs )
140
+ |> Task . async_stream ( & format_file ( & 1 , opts ) , ordered: false , timeout: 30000 )
128
141
|> Enum . reduce ( { [ ] , [ ] , [ ] } , & collect_status / 2 )
129
142
|> check! ( )
130
143
end
@@ -142,71 +155,110 @@ defmodule Mix.Tasks.Format do
142
155
end
143
156
end
144
157
145
- # This function reads exported configuration from the imported dependencies and deals with
146
- # caching the result of reading such configuration in a manifest file.
147
- defp fetch_deps_opts ( dot_formatter , formatter_opts ) do
158
+ # This function reads exported configuration from the imported
159
+ # dependencies and subdirectories and deals with caching the result
160
+ # of reading such configuration in a manifest file.
161
+ defp eval_deps_and_subdirectories ( dot_formatter , prefix , formatter_opts , sources ) do
148
162
deps = Keyword . get ( formatter_opts , :import_deps , [ ] )
163
+ subs = Keyword . get ( formatter_opts , :subdirectories , [ ] )
149
164
150
- cond do
151
- deps == [ ] ->
152
- formatter_opts
153
-
154
- is_list ( deps ) ->
155
- # Since we have dependencies listed, we write the manifest even if those
156
- # dependencies don't export anything so that we avoid lookups everytime.
157
- deps_manifest = Path . join ( Mix.Project . manifest_path ( ) , @ deps_manifest )
158
- dep_parenless_calls = maybe_cache_eval_deps_opts ( dot_formatter , deps_manifest , deps )
159
-
160
- Keyword . update (
161
- formatter_opts ,
162
- :locals_without_parens ,
163
- dep_parenless_calls ,
164
- & ( & 1 ++ dep_parenless_calls )
165
- )
165
+ if not is_list ( deps ) do
166
+ Mix . raise ( "Expected :import_deps to return a list of dependencies, got: #{ inspect ( deps ) } " )
167
+ end
166
168
167
- true ->
168
- Mix . raise ( "Expected :import_deps to return a list of dependencies, got: #{ inspect ( deps ) } " )
169
+ if not is_list ( subs ) do
170
+ Mix . raise ( "Expected :subdirectories to return a list of directories, got: #{ inspect ( subs ) } " )
171
+ end
172
+
173
+ if deps == [ ] and subs == [ ] do
174
+ { { formatter_opts , [ ] } , sources }
175
+ else
176
+ manifest = Path . join ( Mix.Project . manifest_path ( ) , @ manifest )
177
+
178
+ maybe_cache_in_manifest ( dot_formatter , manifest , fn ->
179
+ { subdirectories , sources } = eval_subs_opts ( subs , prefix , sources )
180
+ { { eval_deps_opts ( formatter_opts , deps ) , subdirectories } , sources }
181
+ end )
169
182
end
170
183
end
171
184
172
- defp maybe_cache_eval_deps_opts ( dot_formatter , deps_manifest , deps ) do
185
+ defp maybe_cache_in_manifest ( dot_formatter , manifest , fun ) do
173
186
cond do
174
- dot_formatter != ".formatter.exs" ->
175
- eval_deps_opts ( deps )
176
-
177
- deps_dot_formatters_stale? ( dot_formatter , deps_manifest ) ->
178
- write_deps_manifest ( deps_manifest , eval_deps_opts ( deps ) )
187
+ is_nil ( Mix.Project . get ( ) ) or dot_formatter != ".formatter.exs" -> fun . ( )
188
+ entry = read_manifest ( manifest ) -> entry
189
+ true -> write_manifest! ( manifest , fun . ( ) )
190
+ end
191
+ end
179
192
180
- true ->
181
- read_deps_manifest ( deps_manifest )
193
+ def read_manifest ( manifest ) do
194
+ with { :ok , binary } <- File . read ( manifest ) ,
195
+ { :ok , { @ manifest_vsn , entry , sources } } <- safe_binary_to_term ( binary ) ,
196
+ expanded_sources = Enum . flat_map ( sources , & Path . wildcard ( & 1 , match_dot: true ) ) ,
197
+ false <- Mix.Utils . stale? ( Mix.Project . config_files ( ) ++ expanded_sources , [ manifest ] ) do
198
+ { entry , sources }
199
+ else
200
+ _ -> nil
182
201
end
183
202
end
184
203
185
- defp deps_dot_formatters_stale? ( dot_formatter , deps_manifest ) do
186
- Mix.Utils . stale? ( [ dot_formatter | Mix.Project . config_files ( ) ] , [ deps_manifest ] )
204
+ defp safe_binary_to_term ( binary ) do
205
+ { :ok , :erlang . binary_to_term ( binary ) }
206
+ rescue
207
+ _ -> :error
187
208
end
188
209
189
- defp read_deps_manifest ( deps_manifest ) do
190
- deps_manifest |> File . read! ( ) |> :erlang . binary_to_term ( )
210
+ defp write_manifest! ( manifest , { entry , sources } ) do
211
+ File . mkdir_p! ( Path . dirname ( manifest ) )
212
+ File . write! ( manifest , :erlang . term_to_binary ( { @ manifest_vsn , entry , sources } ) )
213
+ { entry , sources }
191
214
end
192
215
193
- defp write_deps_manifest ( deps_manifest , parenless_calls ) do
194
- File . mkdir_p! ( Path . dirname ( deps_manifest ) )
195
- File . write! ( deps_manifest , :erlang . term_to_binary ( parenless_calls ) )
196
- parenless_calls
216
+ defp eval_deps_opts ( formatter_opts , [ ] ) do
217
+ formatter_opts
197
218
end
198
219
199
- defp eval_deps_opts ( deps ) do
220
+ defp eval_deps_opts ( formatter_opts , deps ) do
200
221
deps_paths = Mix.Project . deps_paths ( )
201
222
202
- for dep <- deps ,
203
- dep_path = assert_valid_dep_and_fetch_path ( dep , deps_paths ) ,
204
- dep_dot_formatter = Path . join ( dep_path , ".formatter.exs" ) ,
205
- File . regular? ( dep_dot_formatter ) ,
206
- dep_opts = eval_file_with_keyword_list ( dep_dot_formatter ) ,
207
- parenless_call <- dep_opts [ :export ] [ :locals_without_parens ] || [ ] ,
208
- uniq: true ,
209
- do: parenless_call
223
+ parenless_calls =
224
+ for dep <- deps ,
225
+ dep_path = assert_valid_dep_and_fetch_path ( dep , deps_paths ) ,
226
+ dep_dot_formatter = Path . join ( dep_path , ".formatter.exs" ) ,
227
+ File . regular? ( dep_dot_formatter ) ,
228
+ dep_opts = eval_file_with_keyword_list ( dep_dot_formatter ) ,
229
+ parenless_call <- dep_opts [ :export ] [ :locals_without_parens ] || [ ] ,
230
+ uniq: true ,
231
+ do: parenless_call
232
+
233
+ Keyword . update (
234
+ formatter_opts ,
235
+ :locals_without_parens ,
236
+ parenless_calls ,
237
+ & ( & 1 ++ parenless_calls )
238
+ )
239
+ end
240
+
241
+ defp eval_subs_opts ( subs , prefix , sources ) do
242
+ { subs , sources } =
243
+ Enum . flat_map_reduce ( subs , sources , fn sub , sources ->
244
+ prefix = Path . join ( prefix ++ [ sub ] )
245
+ { Path . wildcard ( prefix ) , [ Path . join ( prefix , ".formatter.exs" ) | sources ] }
246
+ end )
247
+
248
+ Enum . flat_map_reduce ( subs , sources , fn sub , sources ->
249
+ sub_formatter = Path . join ( sub , ".formatter.exs" )
250
+
251
+ if File . exists? ( sub_formatter ) do
252
+ formatter_opts = eval_file_with_keyword_list ( sub_formatter )
253
+
254
+ { formatter_opts_and_subs , sources } =
255
+ eval_deps_and_subdirectories ( :in_memory , [ sub ] , formatter_opts , sources )
256
+
257
+ { [ { sub , formatter_opts_and_subs } ] , sources }
258
+ else
259
+ { [ ] , sources }
260
+ end
261
+ end )
210
262
end
211
263
212
264
defp assert_valid_dep_and_fetch_path ( dep , deps_paths ) when is_atom ( dep ) do
@@ -243,22 +295,20 @@ defmodule Mix.Tasks.Format do
243
295
opts
244
296
end
245
297
246
- defp expand_args ( [ ] , formatter_opts ) do
247
- if inputs = formatter_opts [ :inputs ] do
248
- expand_files_and_patterns ( List . wrap ( inputs ) , ".formatter.exs" )
249
- else
298
+ defp expand_args ( [ ] , dot_formatter , formatter_opts_and_subs ) do
299
+ if no_entries_in_formatter_opts? ( formatter_opts_and_subs ) do
250
300
Mix . raise (
251
301
"Expected one or more files/patterns to be given to mix format " <>
252
- "or for a .formatter.exs to exist with an :inputs key"
302
+ "or for a .formatter.exs to exist with an :inputs or :subdirectories key"
253
303
)
254
304
end
255
- end
256
305
257
- defp expand_args ( files_and_patterns , _formatter_opts ) do
258
- expand_files_and_patterns ( files_and_patterns , "command line" )
306
+ dot_formatter
307
+ |> expand_dot_inputs ( [ ] , formatter_opts_and_subs , % { } )
308
+ |> Enum . uniq ( )
259
309
end
260
310
261
- defp expand_files_and_patterns ( files_and_patterns , context ) do
311
+ defp expand_args ( files_and_patterns , _dot_formatter , formatter_opts_and_subs ) do
262
312
files =
263
313
for file_or_pattern <- files_and_patterns ,
264
314
file <- stdin_or_wildcard ( file_or_pattern ) ,
@@ -267,12 +317,48 @@ defmodule Mix.Tasks.Format do
267
317
268
318
if files == [ ] do
269
319
Mix . raise (
270
- "Could not find a file to format. The files/patterns from #{ context } " <>
320
+ "Could not find a file to format. The files/patterns given to command line " <>
271
321
"did not point to any existing file. Got: #{ inspect ( files_and_patterns ) } "
272
322
)
273
323
end
274
324
275
- files
325
+ for file <- files do
326
+ if file == :stdin do
327
+ { file , [ ] }
328
+ else
329
+ split = file |> Path . relative_to_cwd ( ) |> Path . split ( )
330
+ { file , find_formatter_opts_for_file ( split , formatter_opts_and_subs ) }
331
+ end
332
+ end
333
+ end
334
+
335
+ defp expand_dot_inputs ( dot_formatter , prefix , { formatter_opts , subs } , acc ) do
336
+ if no_entries_in_formatter_opts? ( { formatter_opts , subs } ) do
337
+ Mix . raise ( "Expected :inputs or :subdirectories key in #{ dot_formatter } " )
338
+ end
339
+
340
+ map =
341
+ for input <- List . wrap ( formatter_opts [ :inputs ] ) ,
342
+ file <- Path . wildcard ( Path . join ( prefix ++ [ input ] ) ) ,
343
+ do: { file , formatter_opts } ,
344
+ into: % { }
345
+
346
+ Enum . reduce ( subs , Map . merge ( acc , map ) , fn { sub , formatter_opts_and_subs } , acc ->
347
+ sub_formatter = Path . join ( sub , ".formatter.exs" )
348
+ expand_dot_inputs ( sub_formatter , [ sub ] , formatter_opts_and_subs , acc )
349
+ end )
350
+ end
351
+
352
+ defp find_formatter_opts_for_file ( split , { formatter_opts , subs } ) do
353
+ Enum . find_value ( subs , formatter_opts , fn { sub , formatter_opts_and_subs } ->
354
+ if List . starts_with? ( split , Path . split ( sub ) ) do
355
+ find_formatter_opts_for_file ( split , formatter_opts_and_subs )
356
+ end
357
+ end )
358
+ end
359
+
360
+ defp no_entries_in_formatter_opts? ( { formatter_opts , subs } ) do
361
+ is_nil ( formatter_opts [ :inputs ] ) and subs == [ ]
276
362
end
277
363
278
364
defp stdin_or_wildcard ( "-" ) , do: [ :stdin ]
@@ -286,7 +372,7 @@ defmodule Mix.Tasks.Format do
286
372
{ File . read! ( file ) , file: file }
287
373
end
288
374
289
- defp format_file ( file , task_opts , formatter_opts ) do
375
+ defp format_file ( { file , formatter_opts } , task_opts ) do
290
376
{ input , extra_opts } = read_file ( file )
291
377
output = IO . iodata_to_binary ( [ Code . format_string! ( input , extra_opts ++ formatter_opts ) , ?\n ] )
292
378
0 commit comments