Skip to content

Commit 6f2e24c

Browse files
committed
Add source/1 to IEx
1 parent 8cc6938 commit 6f2e24c

3 files changed

Lines changed: 139 additions & 29 deletions

File tree

lib/iex/lib/iex/helpers.ex

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ defmodule IEx.Helpers do
5151
* `ref/1` - creates a reference from a string
5252
* `ref/4` - creates a reference with the 4 integer arguments passed
5353
* `runtime_info/0` - prints runtime info (versions, memory usage, stats)
54+
* `source/1` - prints the source location for the given module or function
5455
* `t/1` - prints the types for the given module or function
5556
* `v/0` - retrieves the last value from the history
5657
* `v/1` - retrieves the nth value from the history
@@ -343,6 +344,22 @@ defmodule IEx.Helpers do
343344
end
344345
end
345346

347+
@doc """
348+
Prints the source code for the given module or function.
349+
350+
## Examples
351+
352+
iex> source(MyApp)
353+
iex> source(MyApp.fun/2)
354+
355+
"""
356+
@doc since: "1.20.0"
357+
defmacro source(term) do
358+
quote do
359+
IEx.Introspection.source(unquote(decompose(term, __CALLER__)))
360+
end
361+
end
362+
346363
@doc """
347364
Prints the documentation for `IEx.Helpers`.
348365
"""
@@ -476,7 +493,7 @@ defmodule IEx.Helpers do
476493
raise ArgumentError, "could not load nor find module: #{inspect(module)}"
477494
end
478495

479-
source = source(module)
496+
source = source_path(module)
480497

481498
cond do
482499
source == nil ->
@@ -1008,7 +1025,7 @@ defmodule IEx.Helpers do
10081025
end
10091026
end
10101027

1011-
defp source(module) do
1028+
defp source_path(module) do
10121029
source = module.module_info(:compile)[:source]
10131030

10141031
case source do

lib/iex/lib/iex/introspection.ex

Lines changed: 82 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -104,41 +104,27 @@ defmodule IEx.Introspection do
104104
Opens the given module, mfa, file/line, binary.
105105
"""
106106
def open(module) when is_atom(module) do
107-
case open_mfa(module, :__info__, 1) do
108-
{source, nil, _} -> open(source)
109-
{_, tuple, _} -> open(tuple)
107+
case source_location(module) do
108+
{:ok, tuple} -> open(tuple)
110109
{:error, reason} -> puts_error("Could not open #{inspect(module)}, #{reason}")
111110
end
112111

113112
dont_display_result()
114113
end
115114

116115
def open({module, function}) when is_atom(module) and is_atom(function) do
117-
case open_mfa(module, function, :*) do
118-
{_, _, nil} ->
119-
puts_error(
120-
"Could not open #{inspect(module)}.#{function}, function/macro is not available"
121-
)
122-
123-
{_, _, tuple} ->
124-
open(tuple)
125-
126-
{:error, reason} ->
127-
puts_error("Could not open #{inspect(module)}.#{function}, #{reason}")
116+
case source_location({module, function}) do
117+
{:ok, tuple} -> open(tuple)
118+
{:error, reason} -> puts_error("Could not open #{inspect(module)}.#{function}, #{reason}")
128119
end
129120

130121
dont_display_result()
131122
end
132123

133124
def open({module, function, arity})
134125
when is_atom(module) and is_atom(function) and is_integer(arity) do
135-
case open_mfa(module, function, arity) do
136-
{_, _, nil} ->
137-
puts_error(
138-
"Could not open #{inspect(module)}.#{function}/#{arity}, function/macro is not available"
139-
)
140-
141-
{_, _, tuple} ->
126+
case source_location({module, function, arity}) do
127+
{:ok, tuple} ->
142128
open(tuple)
143129

144130
{:error, reason} ->
@@ -181,13 +167,83 @@ defmodule IEx.Introspection do
181167
dont_display_result()
182168
end
183169

184-
defp open_mfa(module, fun, arity) do
170+
@doc """
171+
Prints source code.
172+
"""
173+
def source(module) when is_atom(module) do
174+
case source_location(module) do
175+
{:ok, {file, _line}} ->
176+
IO.puts(File.read!(file))
177+
178+
{:error, reason} ->
179+
puts_error("Could not show source for #{inspect(module)}, #{reason}")
180+
end
181+
182+
dont_display_result()
183+
end
184+
185+
def source({module, function}) when is_atom(module) and is_atom(function) do
186+
case source_location({module, function}) do
187+
{:ok, {file, _line}} ->
188+
IO.puts(File.read!(file))
189+
190+
{:error, reason} ->
191+
puts_error("Could not show source for #{inspect(module)}.#{function}, #{reason}")
192+
end
193+
194+
dont_display_result()
195+
end
196+
197+
def source({module, function, arity})
198+
when is_atom(module) and is_atom(function) and is_integer(arity) do
199+
case source_location({module, function, arity}) do
200+
{:ok, {file, _line}} ->
201+
IO.puts(File.read!(file))
202+
203+
{:error, reason} ->
204+
puts_error("Could not show source for #{inspect(module)}.#{function}/#{arity}, #{reason}")
205+
end
206+
207+
dont_display_result()
208+
end
209+
210+
def source(invalid) do
211+
puts_error("Invalid arguments for source helper: #{inspect(invalid)}")
212+
dont_display_result()
213+
end
214+
215+
defp source_location(module) when is_atom(module) do
216+
case source_mfa(module, :__info__, 1) do
217+
{source, nil, _} -> {:ok, {source, 1}}
218+
{_, tuple, _} -> {:ok, tuple}
219+
{:error, reason} -> {:error, reason}
220+
end
221+
end
222+
223+
defp source_location({module, function}) when is_atom(module) and is_atom(function) do
224+
case source_mfa(module, function, :*) do
225+
{_, _, nil} -> {:error, "function/macro is not available"}
226+
{_, _, tuple} -> {:ok, tuple}
227+
{:error, reason} -> {:error, reason}
228+
end
229+
end
230+
231+
defp source_location({module, function, arity})
232+
when is_atom(module) and is_atom(function) and is_integer(arity) do
233+
case source_mfa(module, function, arity) do
234+
{_, _, nil} -> {:error, "function/macro is not available"}
235+
{_, _, tuple} -> {:ok, tuple}
236+
{:error, reason} -> {:error, reason}
237+
end
238+
end
239+
240+
defp source_mfa(module, fun, arity) do
185241
case Code.ensure_loaded(module) do
186242
{:module, _} ->
187243
case module.module_info(:compile)[:source] do
188244
[_ | _] = source ->
189245
source = rewrite_source(module, source)
190-
open_abstract_code(module, fun, arity, source)
246+
source_abstract_code(module, fun, arity, source)
191247

192248
_ ->
193249
{:error, "source code is not available"}
@@ -198,14 +254,14 @@ defmodule IEx.Introspection do
198254
end
199255
end
200256

201-
defp open_abstract_code(module, fun, arity, source) do
257+
defp source_abstract_code(module, fun, arity, source) do
202258
fun = Atom.to_string(fun)
203259

204260
with [_ | _] = beam <- :code.which(module),
205261
{:ok, {_, [abstract_code: abstract_code]}} <- :beam_lib.chunks(beam, [:abstract_code]),
206262
{:raw_abstract_v1, code} <- abstract_code do
207263
{_, module_pair, fa_pair} =
208-
Enum.reduce(code, {source, nil, nil}, &open_abstract_code_reduce(&1, &2, fun, arity))
264+
Enum.reduce(code, {source, nil, nil}, &source_abstract_code_reduce(&1, &2, fun, arity))
209265

210266
{source, module_pair, fa_pair}
211267
else
@@ -214,7 +270,7 @@ defmodule IEx.Introspection do
214270
end
215271
end
216272

217-
defp open_abstract_code_reduce(entry, {file, module_pair, fa_pair}, fun, arity) do
273+
defp source_abstract_code_reduce(entry, {file, module_pair, fa_pair}, fun, arity) do
218274
case entry do
219275
{:attribute, ann, :module, _} ->
220276
{file, {file, :erl_anno.line(ann)}, fa_pair}

lib/iex/test/iex/helpers_test.exs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ defmodule IEx.HelpersTest do
283283

284284
test "errors if module is in-memory" do
285285
assert capture_iex("defmodule Foo, do: nil ; open(Foo)") =~
286-
~r"Invalid arguments for open helper:"
286+
"file is not available"
287287
after
288288
cleanup_modules([Foo])
289289
end
@@ -315,6 +315,43 @@ defmodule IEx.HelpersTest do
315315
end
316316
end
317317

318+
describe "source" do
319+
@describetag :requires_source
320+
321+
test "prints source for Elixir module" do
322+
assert capture_iex("source(HelperExampleModule)") =~ "defmodule HelperExampleModule"
323+
end
324+
325+
test "prints source for module.function" do
326+
assert capture_iex("source(HelperExampleModule.fun)") =~ "defmodule HelperExampleModule"
327+
end
328+
329+
test "prints source for module.function/arity" do
330+
assert capture_iex("source(HelperExampleModule.fun/1)") =~ "defmodule HelperExampleModule"
331+
end
332+
333+
test "errors if module is not available" do
334+
assert capture_iex("source(:unknown)") ==
335+
"Could not show source for :unknown, module is not available"
336+
end
337+
338+
test "errors if module.function is not available" do
339+
assert capture_iex("source(:unknown.unknown)") ==
340+
"Could not show source for :unknown.unknown, module is not available"
341+
342+
assert capture_iex("source(:elixir.unknown)") ==
343+
"Could not show source for :elixir.unknown, function/macro is not available"
344+
end
345+
346+
test "errors if module.function/arity is not available" do
347+
assert capture_iex("source(:unknown.start/10)") ==
348+
"Could not show source for :unknown.start/10, module is not available"
349+
350+
assert capture_iex("source(:elixir.start/10)") ==
351+
"Could not show source for :elixir.start/10, function/macro is not available"
352+
end
353+
end
354+
318355
describe "clear" do
319356
test "clear the screen with ansi" do
320357
Application.put_env(:elixir, :ansi_enabled, true)

0 commit comments

Comments
 (0)