@@ -22,6 +22,16 @@ defmodule Mix.Tasks.Format do
2222 * `:inputs` (a list of paths and patterns) - specifies the default inputs
2323 to be used by this task. For example, `["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]`.
2424
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 `lib/app/.formatter.exs`.
31+ Note that the parent `.formatter.exs` must not specify files inside the "lib/app"
32+ subdirectory in its `:inputs` configuration. If this happens, the behaviour of
33+ which formatter configuration will be picked is unspecified.
34+
2535 * `:import_deps` (a list of dependencies as atoms) - specifies a list
2636 of dependencies whose formatter configuration will be imported.
2737 When specified, the formatter should run in the same directory as
@@ -115,16 +125,19 @@ defmodule Mix.Tasks.Format do
115125 dry_run: :boolean
116126 ]
117127
118- @ deps_manifest "cached_formatter_deps"
128+ @ manifest "cached_dot_formatter"
129+ @ manifest_vsn 1
119130
120131 def run ( args ) do
121132 { opts , args } = OptionParser . parse! ( args , strict: @ switches )
122133 { 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 ] )
124137
125138 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 )
128141 |> Enum . reduce ( { [ ] , [ ] , [ ] } , & collect_status / 2 )
129142 |> check! ( )
130143 end
@@ -142,71 +155,110 @@ defmodule Mix.Tasks.Format do
142155 end
143156 end
144157
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
148162 deps = Keyword . get ( formatter_opts , :import_deps , [ ] )
163+ subs = Keyword . get ( formatter_opts , :subdirectories , [ ] )
149164
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
166168
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 )
169182 end
170183 end
171184
172- defp maybe_cache_eval_deps_opts ( dot_formatter , deps_manifest , deps ) do
185+ defp maybe_cache_in_manifest ( dot_formatter , manifest , fun ) do
173186 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
179192
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
182201 end
183202 end
184203
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
187208 end
188209
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 }
191214 end
192215
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
197218 end
198219
199- defp eval_deps_opts ( deps ) do
220+ defp eval_deps_opts ( formatter_opts , deps ) do
200221 deps_paths = Mix.Project . deps_paths ( )
201222
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 )
210262 end
211263
212264 defp assert_valid_dep_and_fetch_path ( dep , deps_paths ) when is_atom ( dep ) do
@@ -243,22 +295,20 @@ defmodule Mix.Tasks.Format do
243295 opts
244296 end
245297
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
250300 Mix . raise (
251301 "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"
253303 )
254304 end
255- end
256305
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 ( )
259309 end
260310
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
262312 files =
263313 for file_or_pattern <- files_and_patterns ,
264314 file <- stdin_or_wildcard ( file_or_pattern ) ,
@@ -267,12 +317,48 @@ defmodule Mix.Tasks.Format do
267317
268318 if files == [ ] do
269319 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 " <>
271321 "did not point to any existing file. Got: #{ inspect ( files_and_patterns ) } "
272322 )
273323 end
274324
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 == [ ]
276362 end
277363
278364 defp stdin_or_wildcard ( "-" ) , do: [ :stdin ]
@@ -286,7 +372,7 @@ defmodule Mix.Tasks.Format do
286372 { File . read! ( file ) , file: file }
287373 end
288374
289- defp format_file ( file , task_opts , formatter_opts ) do
375+ defp format_file ( { file , formatter_opts } , task_opts ) do
290376 { input , extra_opts } = read_file ( file )
291377 output = IO . iodata_to_binary ( [ Code . format_string! ( input , extra_opts ++ formatter_opts ) , ?\n ] )
292378
0 commit comments