@@ -36,18 +36,24 @@ defmodule Mix.Tasks.Format do
36
36
If any of the `--check-*` flags are given and a check fails, the formatted
37
37
contents won't be written to disk nor printed to stdout.
38
38
39
- ## .formatter.exs
39
+ ## ` .formatter.exs`
40
40
41
41
The formatter will read a `.formatter.exs` in the current directory for
42
42
formatter configuration. It should return a keyword list with any of the
43
43
options supported by `Code.format_string!/2`.
44
44
45
- The `.formatter.exs` also supports an `:inputs` field which specifies the
46
- default inputs to be used by this task:
45
+ The `.formatter.exs` also supports other options:
47
46
48
- [
49
- inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]
50
- ]
47
+ * `:inputs` (a list of paths and patterns) - specifies the default inputs
48
+ to be used by this task. For example, `["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]`.
49
+
50
+ * `:import_deps` (a list of dependencies as atoms) - specifies a list
51
+ of dependencies whose formatter configuration will be imported.
52
+ See the "Importing dependencies configuration" section below for more
53
+ information.
54
+
55
+ * `:export` (a keyword list) - specifies formatter configuration to be exported. See the
56
+ "Importing dependencies configuration" section below.
51
57
52
58
## When to format code
53
59
@@ -65,6 +71,43 @@ defmodule Mix.Tasks.Format do
65
71
of patterns and files to `mix format`, as showed at the top of this task
66
72
documentation. This list can also be set in the `.formatter.exs` under the
67
73
`:inputs` key.
74
+
75
+ ## Importing dependencies configuration
76
+
77
+ This task supports importing formatter configuration from dependencies.
78
+
79
+ A dependency that wants to export formatter configuration needs to have a `.formatter.exs` file
80
+ at the root of the project. In this file, the dependency can export a `:export` option with
81
+ configuration to export. For now, only one option is supported under `:export`:
82
+ `:export_locals_without_parens` (whose value has the same shape as the value of the
83
+ `:locals_without_parens` in `Code.format_string!/2`).
84
+
85
+ The functions listed under `:locals_without_parens` in the `:export` option of a dependency
86
+ can be imported in a project by listing that dependency in the `:import_deps`
87
+ option of the formatter configuration file of the project.
88
+
89
+ For example, consider I have a project `my_app` that depends on `my_dep`.
90
+ `my_dep` wants to export some configuration, so `my_dep/.formatter.exs`
91
+ would look like this:
92
+
93
+ # my_dep/.formatter.exs
94
+ [
95
+ # Regular formatter configuration for my_dep
96
+ # ...
97
+
98
+ export: [
99
+ locals_without_parens: [some_dsl_call: 2, some_dsl_call: 3]
100
+ ]
101
+ ]
102
+
103
+ In order to import configuration, `my_app`'s `.formatter.exs` would look like
104
+ this:
105
+
106
+ # my_app/.formatter.exs
107
+ [
108
+ import_deps: [:my_dep]
109
+ ]
110
+
68
111
"""
69
112
70
113
@ switches [
@@ -74,9 +117,12 @@ defmodule Mix.Tasks.Format do
74
117
dry_run: :boolean
75
118
]
76
119
120
+ @ deps_manifest "cached_formatter_deps"
121
+
77
122
def run ( args ) do
78
123
{ opts , args } = OptionParser . parse! ( args , strict: @ switches )
79
124
formatter_opts = eval_dot_formatter ( opts )
125
+ formatter_opts = fetch_deps_opts ( formatter_opts )
80
126
81
127
args
82
128
|> expand_args ( formatter_opts )
@@ -87,20 +133,8 @@ defmodule Mix.Tasks.Format do
87
133
88
134
defp eval_dot_formatter ( opts ) do
89
135
case dot_formatter ( opts ) do
90
- { :ok , dot_formatter } ->
91
- { formatter_opts , _ } = Code . eval_file ( dot_formatter )
92
-
93
- unless Keyword . keyword? ( formatter_opts ) do
94
- Mix . raise (
95
- "Expected #{ inspect ( dot_formatter ) } to return a keyword list, " <>
96
- "got: #{ inspect ( formatter_opts ) } "
97
- )
98
- end
99
-
100
- formatter_opts
101
-
102
- :error ->
103
- [ ]
136
+ { :ok , dot_formatter } -> eval_file_with_keyword_list ( dot_formatter )
137
+ :error -> [ ]
104
138
end
105
139
end
106
140
@@ -112,6 +146,93 @@ defmodule Mix.Tasks.Format do
112
146
end
113
147
end
114
148
149
+ # This function reads exported configuration from the imported dependencies and deals with
150
+ # caching the result of reading such configuration in a manifest file.
151
+ defp fetch_deps_opts ( formatter_opts ) do
152
+ deps = Keyword . get ( formatter_opts , :import_deps , [ ] )
153
+
154
+ cond do
155
+ deps == [ ] ->
156
+ formatter_opts
157
+
158
+ is_list ( deps ) ->
159
+ # Since we have dependencies listed, we write the manifest even if those dependencies
160
+ # don't export anything so that we avoid lookups everytime.
161
+ deps_manifest = Path . join ( Mix.Project . manifest_path ( ) , @ deps_manifest )
162
+
163
+ dep_parenless_calls =
164
+ if deps_dot_formatters_stale? ( deps_manifest ) do
165
+ dep_parenless_calls = eval_deps_opts ( deps )
166
+ write_deps_manifest ( deps_manifest , dep_parenless_calls )
167
+ dep_parenless_calls
168
+ else
169
+ read_deps_manifest ( deps_manifest )
170
+ end
171
+
172
+ Keyword . update (
173
+ formatter_opts ,
174
+ :locals_without_parens ,
175
+ dep_parenless_calls ,
176
+ & ( & 1 ++ dep_parenless_calls )
177
+ )
178
+
179
+ true ->
180
+ Mix . raise ( "Expected :import_deps to return a list of dependencies, got: #{ inspect ( deps ) } " )
181
+ end
182
+ end
183
+
184
+ defp deps_dot_formatters_stale? ( deps_manifest ) do
185
+ Mix.Utils . stale? ( [ ".formatter.exs" | Mix.Project . config_files ( ) ] , [ deps_manifest ] )
186
+ end
187
+
188
+ defp read_deps_manifest ( deps_manifest ) do
189
+ deps_manifest |> File . read! ( ) |> :erlang . binary_to_term ( )
190
+ end
191
+
192
+ defp write_deps_manifest ( deps_manifest , parenless_calls ) do
193
+ File . mkdir_p! ( Path . dirname ( deps_manifest ) )
194
+ File . write! ( deps_manifest , :erlang . term_to_binary ( parenless_calls ) )
195
+ end
196
+
197
+ defp eval_deps_opts ( deps ) do
198
+ deps_paths = Mix.Project . deps_paths ( )
199
+
200
+ for dep <- deps ,
201
+ dep_path = assert_valid_dep_and_fetch_path ( dep , deps_paths ) ,
202
+ dep_dot_formatter = Path . join ( dep_path , ".formatter.exs" ) ,
203
+ File . regular? ( dep_dot_formatter ) ,
204
+ dep_opts = eval_file_with_keyword_list ( dep_dot_formatter ) ,
205
+ parenless_call <- dep_opts [ :export ] [ :locals_without_parens ] || [ ] ,
206
+ uniq: true ,
207
+ do: parenless_call
208
+ end
209
+
210
+ defp assert_valid_dep_and_fetch_path ( dep , deps_paths ) when is_atom ( dep ) do
211
+ case Map . fetch ( deps_paths , dep ) do
212
+ { :ok , path } ->
213
+ path
214
+
215
+ :error ->
216
+ Mix . raise (
217
+ "Found a dependency in :import_deps that the project doesn't depend on: #{ inspect ( dep ) } "
218
+ )
219
+ end
220
+ end
221
+
222
+ defp assert_valid_dep_and_fetch_path ( dep , _deps_paths ) do
223
+ Mix . raise ( "Dependencies in :import_deps should be atoms, got: #{ inspect ( dep ) } " )
224
+ end
225
+
226
+ defp eval_file_with_keyword_list ( path ) do
227
+ { opts , _ } = Code . eval_file ( path )
228
+
229
+ unless Keyword . keyword? ( opts ) do
230
+ Mix . raise ( "Expected #{ inspect ( path ) } to return a keyword list, got: #{ inspect ( opts ) } " )
231
+ end
232
+
233
+ opts
234
+ end
235
+
115
236
defp expand_args ( [ ] , formatter_opts ) do
116
237
if inputs = formatter_opts [ :inputs ] do
117
238
expand_files_and_patterns ( List . wrap ( inputs ) , ".formatter.exs" )
0 commit comments