Skip to content

Commit bba4f1b

Browse files
author
José Valim
committed
Optimize and add specs to Mix.Task module
1 parent ade2596 commit bba4f1b

File tree

8 files changed

+87
-60
lines changed

8 files changed

+87
-60
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ $(1): lib/$(1)/ebin/Elixir.$(2).beam lib/$(1)/ebin/$(1).app
3232
lib/$(1)/ebin/$(1).app: lib/$(1)/mix.exs
3333
$(Q) mkdir -p lib/$(1)/_build/shared/lib/$(1)
3434
$(Q) cp -R lib/$(1)/ebin lib/$(1)/_build/shared/lib/$(1)/
35-
$(Q) cd lib/$(1) && ../../bin/elixir -e "Mix.start(:permanent, [])" -r mix.exs -e "Mix.Task.run('compile.app')"
35+
$(Q) cd lib/$(1) && ../../bin/elixir -e 'Mix.start(:permanent, [])' -r mix.exs -e 'Mix.Task.run("compile.app")'
3636
$(Q) cp lib/$(1)/_build/shared/lib/$(1)/ebin/$(1).app lib/$(1)/ebin/$(1).app
3737
$(Q) rm -rf lib/$(1)/_build
3838

lib/mix/lib/mix/cli.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ defmodule Mix.CLI do
6060
# If the task is not available, let's try to
6161
# compile the repository and then run it again.
6262
cond do
63-
Mix.Task.get(name) ->
63+
match?({:ok, _}, Mix.Task.get(name)) ->
6464
Mix.Task.run(name, args)
6565
Mix.Project.get ->
6666
Mix.Task.run("compile")

lib/mix/lib/mix/local.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ defmodule Mix.Local do
2828
@doc """
2929
Returns all tasks in local archives.
3030
"""
31-
def all_tasks, do: Mix.Task.load_tasks(archives_ebin)
31+
def all_tasks do
32+
Mix.Task.load_tasks(archives_ebin)
33+
end
3234

3335
@doc """
3436
Returns paths of all archive files matching given

lib/mix/lib/mix/task.ex

Lines changed: 73 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ defmodule Mix.Task do
3131
3232
"""
3333

34+
@type task_name :: String.t | atom
35+
@type task_module :: atom
36+
3437
@doc """
3538
A task needs to implement `run` which receives
3639
a list of command line args.
@@ -42,58 +45,63 @@ defmodule Mix.Task do
4245
quote do
4346
Enum.each [:shortdoc, :recursive],
4447
&Module.register_attribute(__MODULE__, &1, persist: true)
45-
4648
@behaviour Mix.Task
4749
end
4850
end
4951

5052
@doc """
5153
Loads all tasks in all code paths.
5254
"""
55+
@spec load_all() :: [task_module]
5356
def load_all, do: load_tasks(:code.get_path)
5457

5558
@doc """
5659
Loads all tasks in the given `paths`.
5760
"""
58-
def load_tasks(paths) do
59-
Enum.reduce(paths, [], fn(path, matches) ->
60-
{:ok, files} = :erl_prim_loader.list_dir(path |> to_char_list)
61-
Enum.reduce(files, matches, &match_tasks/2)
62-
end)
61+
@spec load_tasks([List.Chars.t]) :: [task_module]
62+
def load_tasks(dirs) do
63+
for dir <- dirs,
64+
{:ok, files} = :erl_prim_loader.list_dir(to_char_list(dir)),
65+
file <- files,
66+
mod = task_from_path(file),
67+
do: mod
6368
end
6469

65-
@re_pattern Regex.re_pattern(~r/Elixir\.Mix\.Tasks\..*\.beam$/)
70+
@prefix_size byte_size("Elixir.Mix.Tasks.")
71+
@suffix_size byte_size(".beam")
6672

67-
defp match_tasks(filename, modules) do
68-
if :re.run(filename, @re_pattern, [capture: :none]) == :match do
69-
mod = :filename.rootname(filename, '.beam') |> List.to_atom
70-
if Code.ensure_loaded?(mod), do: [mod | modules], else: modules
71-
else
72-
modules
73+
defp task_from_path(filename) do
74+
base = Path.basename(filename)
75+
part = byte_size(base) - @prefix_size - @suffix_size
76+
77+
case base do
78+
<<"Elixir.Mix.Tasks.", rest :: binary-size(part), ".beam">> ->
79+
mod = :"Elixir.Mix.Tasks.#{rest}"
80+
ensure_task?(mod) && mod
81+
_ ->
82+
nil
7383
end
7484
end
7585

7686
@doc """
77-
Returns all loaded tasks.
87+
Returns all loaded task modules.
7888
7989
Modules that are not yet loaded won't show up.
8090
Check `load_all/0` if you want to preload all tasks.
8191
"""
92+
@spec all_modules() :: [task_module]
8293
def all_modules do
83-
Enum.reduce :code.all_loaded, [], fn({module, _}, acc) ->
84-
case Atom.to_char_list(module) do
85-
'Elixir.Mix.Tasks.' ++ _ ->
86-
if is_task?(module), do: [module|acc], else: acc
87-
_ ->
88-
acc
89-
end
90-
end
94+
for {module, _} <- :code.all_loaded,
95+
task?(module),
96+
do: module
9197
end
9298

9399
@doc """
94100
Gets the moduledoc for the given task `module`.
101+
95102
Returns the moduledoc or `nil`.
96103
"""
104+
@spec moduledoc(task_module) :: String.t | nil
97105
def moduledoc(module) when is_atom(module) do
98106
case Code.get_docs(module, :moduledoc) do
99107
{_line, moduledoc} -> moduledoc
@@ -103,8 +111,10 @@ defmodule Mix.Task do
103111

104112
@doc """
105113
Gets the shortdoc for the given task `module`.
114+
106115
Returns the shortdoc or `nil`.
107116
"""
117+
@spec shortdoc(task_module) :: String.t | nil
108118
def shortdoc(module) when is_atom(module) do
109119
case List.keyfind module.__info__(:attributes), :shortdoc, 0 do
110120
{:shortdoc, [shortdoc]} -> shortdoc
@@ -114,8 +124,11 @@ defmodule Mix.Task do
114124

115125
@doc """
116126
Checks if the task should be run recursively for all sub-apps in
117-
umbrella projects. Returns `true`, `false` or `:both`.
127+
umbrella projects.
128+
129+
Returns `true` or `false`.
118130
"""
131+
@spec recursive(task_module) :: boolean
119132
def recursive(module) when is_atom(module) do
120133
case List.keyfind module.__info__(:attributes), :recursive, 0 do
121134
{:recursive, [setting]} -> setting
@@ -126,18 +139,25 @@ defmodule Mix.Task do
126139
@doc """
127140
Returns the task name for the given `module`.
128141
"""
129-
def task_name(module) do
142+
@spec task_name(task_module) :: task_name
143+
def task_name(module) when is_atom(module) do
130144
Mix.Utils.module_name_to_command(module, 2)
131145
end
132146

133147
@doc """
134-
Receives a task name and retrieves the task module.
135-
Returns nil if the task cannot be found.
148+
Receives a task name and returns `{:ok, module}` if a task is found.
149+
150+
Otherwise returns `{:error, :invalid}` in case the module
151+
exists but it isn't a task or `{:error, :not_found}`.
136152
"""
137-
def get(task) do
138-
case Mix.Utils.command_to_module(task, Mix.Tasks) do
139-
{:module, module} -> module
140-
{:error, _} -> nil
153+
@spec get(task_name) :: {:ok, task_module} | {:error, :invalid} | {:error, :not_found}
154+
155+
def get(task) when is_binary(task) or is_atom(task) do
156+
case Mix.Utils.command_to_module(to_string(task), Mix.Tasks) do
157+
{:module, module} ->
158+
if task?(module), do: {:ok, module}, else: {:error, :invalid}
159+
{:error, _} ->
160+
{:error, :not_found}
141161
end
142162
end
143163

@@ -150,15 +170,15 @@ defmodule Mix.Task do
150170
* `Mix.InvalidTaskError` - raised if the task is not a valid `Mix.Task`
151171
152172
"""
173+
@spec get!(task_name) :: task_module | no_return
153174
def get!(task) do
154-
if module = get(task) do
155-
if is_task?(module) do
175+
case get(task) do
176+
{:ok, module} ->
156177
module
157-
else
178+
{:error, :invalid} ->
158179
Mix.raise Mix.InvalidTaskError, task: task
159-
end
160-
else
161-
Mix.raise Mix.NoTaskError, task: task
180+
{:error, :not_found} ->
181+
Mix.raise Mix.NoTaskError, task: task
162182
end
163183
end
164184

@@ -174,7 +194,8 @@ defmodule Mix.Task do
174194
It may raise an exception if the task was not found
175195
or it is invalid. Check `get!/1` for more information.
176196
"""
177-
def run(task, args \\ []) do
197+
@spec run(task_name, [any]) :: any
198+
def run(task, args \\ []) when is_binary(task) or is_atom(task) do
178199
task = to_string(task)
179200

180201
if Mix.TasksServer.run_task(task, Mix.Project.get) do
@@ -192,15 +213,18 @@ defmodule Mix.Task do
192213
@doc """
193214
Clears all invoked tasks, allowing them to be reinvoked.
194215
"""
216+
@spec clear :: :ok
195217
def clear do
196218
Mix.TasksServer.clear_tasks
197219
end
198220

199221
@doc """
200-
Reenables a given task so it can be executed again down the stack. If
201-
an umbrella project reenables a task it is reenabled for all sub projects.
222+
Reenables a given task so it can be executed again down the stack.
223+
224+
If an umbrella project reenables a task it is reenabled for all sub projects.
202225
"""
203-
def reenable(task) do
226+
@spec reenable(task_name) :: :ok
227+
def reenable(task) when is_binary(task) or is_atom(task) do
204228
task = to_string(task)
205229
module = get!(task)
206230

@@ -209,6 +233,8 @@ defmodule Mix.Task do
209233
recur module, fn project ->
210234
Mix.TasksServer.delete_task(task, project)
211235
end
236+
237+
:ok
212238
end
213239

214240
defp recur(module, fun) do
@@ -233,7 +259,12 @@ defmodule Mix.Task do
233259
@doc """
234260
Returns `true` if given module is a task.
235261
"""
236-
def is_task?(module) do
237-
function_exported?(module, :run, 1)
262+
@spec task?(task_module) :: boolean()
263+
def task?(module) when is_atom(module) do
264+
match?('Elixir.Mix.Tasks.' ++ _, Atom.to_char_list(module)) and ensure_task?(module)
265+
end
266+
267+
defp ensure_task?(module) do
268+
Code.ensure_loaded?(module) and function_exported?(module, :run, 1)
238269
end
239270
end

lib/mix/lib/mix/tasks/help.ex

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,11 @@ defmodule Mix.Tasks.Help do
3636
"""
3737

3838
def run([]) do
39-
Mix.Task.load_all
40-
4139
shell = Mix.shell
42-
modules = Mix.Task.all_modules
40+
modules = Mix.Task.load_all()
4341

4442
docs = for module <- modules,
45-
doc = Mix.Task.shortdoc(module) do
43+
doc = Mix.Task.shortdoc(module) do
4644
{"mix " <> Mix.Task.task_name(module), doc}
4745
end
4846

@@ -60,10 +58,8 @@ defmodule Mix.Tasks.Help do
6058
end
6159

6260
def run(["--names"]) do
63-
Mix.Task.load_all
64-
for module <- Enum.sort(Mix.Task.all_modules),
65-
task = Mix.Task.task_name(module) do
66-
Mix.shell.info "#{task}"
61+
for module <- Enum.sort(Mix.Task.load_all()) do
62+
Mix.shell.info "#{Mix.Task.task_name(module)}"
6763
end
6864
end
6965

lib/mix/lib/mix/tasks/local.ex

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ defmodule Mix.Tasks.Local do
1111
shell = Mix.shell
1212
modules = Mix.Local.all_tasks
1313

14-
docs = for module <- modules,
15-
Mix.Task.is_task?(module) do
14+
docs = for module <- modules do
1615
{Mix.Task.task_name(module), Mix.Task.shortdoc(module)}
1716
end
1817

lib/mix/lib/mix/tasks_server.ex

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ defmodule Mix.TasksServer do
66
end
77

88
def clear_tasks() do
9-
get_and_update fn set ->
10-
{ set, HashSet.new }
11-
end
9+
update fn _ -> HashSet.new end
1210
end
1311

1412
def run_task(task, proj) do

lib/mix/test/mix/task_test.exs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@ defmodule Mix.TaskTest do
4848
end
4949

5050
test :clear do
51-
Mix.Task.run("hello")
52-
assert {"hello", nil} in Mix.Task.clear
51+
assert Mix.Task.run("hello") == "Hello, World!"
52+
Mix.Task.clear
53+
assert Mix.Task.run("hello") == "Hello, World!"
5354
end
5455

5556
test :reenable do

0 commit comments

Comments
 (0)