Skip to content

Commit c8a0158

Browse files
author
José Valim
committed
Add @external_resource attribute, closes #2455
1 parent b088d4b commit c8a0158

File tree

6 files changed

+72
-36
lines changed

6 files changed

+72
-36
lines changed

lib/eex/lib/eex.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ defmodule EEx do
8282
iex> EEx.eval_string "<%= @foo %>", assigns: [foo: 1]
8383
"1"
8484
85-
In other words, <%= @foo %> is simply translated to:
85+
In other words, `<%= @foo %>` is simply translated to:
8686
8787
<%= Dict.get assigns, :foo %>
8888
@@ -92,6 +92,7 @@ defmodule EEx do
9292

9393
@doc """
9494
Generates a function definition from the string.
95+
9596
The kind (`:def` or `:defp`) must be given, the
9697
function name, its arguments and the compilation options.
9798
@@ -120,6 +121,7 @@ defmodule EEx do
120121

121122
@doc """
122123
Generates a function definition from the file contents.
124+
123125
The kind (`:def` or `:defp`) must be given, the
124126
function name, its arguments and the compilation options.
125127
@@ -147,6 +149,7 @@ defmodule EEx do
147149
args = Enum.map args, fn arg -> {arg, [line: 1], nil} end
148150
compiled = EEx.compile_file(file, info)
149151

152+
@external_resource file
150153
@file file
151154
case kind do
152155
:def -> def(unquote(name)(unquote_splicing(args)), do: unquote(compiled))

lib/eex/test/eex_test.exs

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ Code.require_file "test_helper.exs", __DIR__
22

33
require EEx
44

5-
defmodule EExText.Compiled do
5+
defmodule EExTest.Compiled do
66
def before_compile do
77
fill_in_stacktrace
88
{__ENV__.line, hd(tl(System.stacktrace))}
@@ -12,6 +12,10 @@ defmodule EExText.Compiled do
1212

1313
filename = Path.join(__DIR__, "fixtures/eex_template_with_bindings.eex")
1414
EEx.function_from_file :defp, :private_file_sample, filename, [:bar]
15+
16+
filename = Path.join(__DIR__, "fixtures/eex_template_with_bindings.eex")
17+
EEx.function_from_file :def, :public_file_sample, filename, [:bar]
18+
1519
def file_sample(arg), do: private_file_sample(arg)
1620

1721
def after_compile do
@@ -319,39 +323,45 @@ foo
319323
end
320324
end
321325

326+
test "sets external resource attribute" do
327+
assert EExTest.Compiled.__info__(:attributes)[:external_resource] ==
328+
[Path.join(__DIR__, "fixtures/eex_template_with_bindings.eex")]
329+
end
330+
322331
test "defined from string" do
323-
assert EExText.Compiled.string_sample(1, 2) == "3"
332+
assert EExTest.Compiled.string_sample(1, 2) == "3"
324333
end
325334

326335
test "defined from file" do
327-
assert EExText.Compiled.file_sample(1) == "foo 1\n"
336+
assert EExTest.Compiled.file_sample(1) == "foo 1\n"
337+
assert EExTest.Compiled.public_file_sample(1) == "foo 1\n"
328338
end
329339

330340
test "defined from file do not affect backtrace" do
331-
assert EExText.Compiled.before_compile ==
341+
assert EExTest.Compiled.before_compile ==
332342
{8,
333-
{EExText.Compiled,
343+
{EExTest.Compiled,
334344
:before_compile,
335345
0,
336346
[file: to_char_list(Path.relative_to_cwd(__ENV__.file)), line: 7]
337347
}
338348
}
339349

340-
assert EExText.Compiled.after_compile ==
341-
{19,
342-
{EExText.Compiled,
350+
assert EExTest.Compiled.after_compile ==
351+
{23,
352+
{EExTest.Compiled,
343353
:after_compile,
344354
0,
345-
[file: to_char_list(Path.relative_to_cwd(__ENV__.file)), line: 18]
355+
[file: to_char_list(Path.relative_to_cwd(__ENV__.file)), line: 22]
346356
}
347357
}
348358

349-
assert EExText.Compiled.unknown ==
350-
{25,
351-
{EExText.Compiled,
359+
assert EExTest.Compiled.unknown ==
360+
{29,
361+
{EExTest.Compiled,
352362
:unknown,
353363
0,
354-
[file: 'unknown', line: 24]
364+
[file: 'unknown', line: 28]
355365
}
356366
}
357367
end

lib/elixir/lib/module.ex

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,17 @@ defmodule Module do
247247
@vsn "1.0"
248248
end
249249
250+
* `@external_resource`
251+
252+
Specify an external resource to the current module.
253+
254+
Many times a module embeds information from an external file. This
255+
attribute allows the module to annotate which external resources
256+
have been used.
257+
258+
Tools like Mix may use this information to ensure the module is
259+
recompiled in case any of the external resources change.
260+
250261
The following attributes are part of typespecs and are also reserved by
251262
Elixir (see `Kernel.Typespec` for more information about typespecs):
252263

lib/elixir/src/elixir_module.erl

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ build(Line, File, Module, Lexical) ->
111111
_ -> ets:insert(DataTable, {on_definition, []})
112112
end,
113113

114-
Attributes = [behaviour, on_load, spec, type, typep, opaque, callback, compile],
114+
Attributes = [behaviour, on_load, spec, type, typep, opaque, callback, compile, external_resource],
115115
ets:insert(DataTable, {?acc_attr, [before_compile, after_compile, on_definition, derive|Attributes]}),
116116
ets:insert(DataTable, {?persisted_attr, [vsn|Attributes]}),
117117
ets:insert(DataTable, {?docs_attr, ets:new(DataTable, [ordered_set, public])}),
@@ -156,7 +156,7 @@ functions_form(Line, File, Module, BaseAll, BaseExport, Def, Defmacro, BaseFunct
156156

157157
%% Add attributes handling to the form
158158

159-
attributes_form(Line, _File, Module, Current) ->
159+
attributes_form(Line, File, Module, Current) ->
160160
Table = data_table(Module),
161161

162162
AccAttrs = ets:lookup_element(Table, '__acc_attributes', 2),
@@ -166,16 +166,31 @@ attributes_form(Line, _File, Module, Current) ->
166166
case lists:member(Key, PersistedAttrs) of
167167
false -> Acc;
168168
true ->
169-
Attrs = case lists:member(Key, AccAttrs) of
170-
true -> Value;
171-
false -> [Value]
172-
end,
173-
lists:foldl(fun(X, Final) -> [{attribute, Line, Key, X}|Final] end, Acc, Attrs)
169+
Values =
170+
case lists:member(Key, AccAttrs) of
171+
true -> Value;
172+
false -> [Value]
173+
end,
174+
175+
lists:foldl(fun(X, Final) ->
176+
[{attribute, Line, Key, X}|Final]
177+
end, Acc, process_attribute(Line, File, Key, Values))
174178
end
175179
end,
176180

177181
ets:foldl(Transform, Current, Table).
178182

183+
process_attribute(Line, File, external_resource, Values) ->
184+
lists:usort([process_external_resource(Line, File, Value) || Value <- Values]);
185+
process_attribute(_Line, _File, _Key, Values) ->
186+
Values.
187+
188+
process_external_resource(_Line, _File, Value) when is_binary(Value) ->
189+
Value;
190+
process_external_resource(Line, File, Value) ->
191+
elixir_errors:handle_file_error(File,
192+
{Line, ?MODULE, {invalid_external_resource, Value}}).
193+
179194
%% Types
180195

181196
types_form(Module, Forms0) ->
@@ -463,14 +478,17 @@ prune_stacktrace(Info, []) ->
463478

464479
format_error({invalid_clause, {Name, Arity}}) ->
465480
io_lib:format("empty clause provided for nonexistent function or macro ~ts/~B", [Name, Arity]);
481+
format_error({invalid_external_resource, Value}) ->
482+
io_lib:format("expected a string value for @external_resource, got: ~p",
483+
['Elixir.Kernel':inspect(Value)]);
466484
format_error({unused_doc, typedoc}) ->
467485
"@typedoc provided but no type follows it";
468486
format_error({unused_doc, doc}) ->
469487
"@doc provided but no definition follows it";
470488
format_error({internal_function_overridden, {Name, Arity}}) ->
471489
io_lib:format("function ~ts/~B is internal and should not be overridden", [Name, Arity]);
472490
format_error({invalid_module, Module}) ->
473-
io_lib:format("invalid module name: ~p", [Module]);
491+
io_lib:format("invalid module name: ~ts", ['Elixir.Kernel':inspect(Module)]);
474492
format_error({module_defined, Module}) ->
475493
io_lib:format("redefining module ~ts", [elixir_aliases:inspect(Module)]);
476494
format_error({module_reserved, Module}) ->

lib/mix/lib/mix/compilers/elixir.ex

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -107,23 +107,17 @@ defmodule Mix.Compilers.Elixir do
107107
|> Enum.map(&Atom.to_string(&1))
108108
|> Enum.reject(&match?("elixir_" <> _, &1))
109109

110-
files = get_beam_files(binary, cwd)
111-
|> List.delete(source)
112-
|> Enum.filter(&(Path.type(&1) == :relative))
110+
files = for file <- get_external_resources(module, cwd),
111+
File.regular?(file),
112+
relative = Path.relative_to(file, cwd),
113+
Path.type(relative) == :relative,
114+
do: relative
113115

114116
Agent.cast pid, &:lists.keystore(beam, 1, &1, {beam, bin, source, deps, files, binary})
115117
end
116118

117-
defp get_beam_files(binary, cwd) do
118-
case :beam_lib.chunks(binary, [:abstract_code]) do
119-
{:ok, {_, [abstract_code: {:raw_abstract_v1, code}]}} ->
120-
for {:attribute, _, :file, {file, _}} <- code,
121-
File.exists?(file) do
122-
Path.relative_to(file, cwd)
123-
end
124-
_ ->
125-
[]
126-
end
119+
defp get_external_resources(module, cwd) do
120+
module.__info__(:attributes)[:external_resource] || []
127121
end
128122

129123
defp each_file(file) do

lib/mix/test/mix/tasks/compile.elixir_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
132132
File.touch!("lib/a.eex")
133133
File.write!("lib/a.ex", """
134134
defmodule A do
135-
@file "lib/a.eex"
135+
@external_resource "lib/a.eex"
136136
def a, do: :ok
137137
end
138138
""")

0 commit comments

Comments
 (0)