Browse Source

Work in progress on implementing #98.

Andreas Stenius 11 years ago
parent
commit
9fedfd8151

+ 33 - 27
README.markdown

@@ -27,7 +27,7 @@ in this directory.
 Template compilation
 Template compilation
 --------------------
 --------------------
 
 
-Four ways:
+Usage:
 
 
     erlydtl:compile("/path/to/template.dtl", my_module_name)
     erlydtl:compile("/path/to/template.dtl", my_module_name)
 
 
@@ -37,6 +37,18 @@ Four ways:
 
 
     erlydtl:compile(<<"<html>{{ foo }}</html>">>, my_module_name, Options)
     erlydtl:compile(<<"<html>{{ foo }}</html>">>, my_module_name, Options)
 
 
+Result:
+
+    ok %% existing compiled template is up to date.
+    
+    {ok, Module}
+    {ok, Module, Warnings}
+    {ok, Module, Binary}
+    {ok, Module, Binary, Warnings}
+    
+    error
+    {error, Errors, Warnings}
+    
 Options is a proplist possibly containing:
 Options is a proplist possibly containing:
 
 
 * `outdir` - Directory to store generated .beam files. If not
 * `outdir` - Directory to store generated .beam files. If not
@@ -86,8 +98,8 @@ Options is a proplist possibly containing:
 * `compiler_options` - Proplist with extra options passed directly to
 * `compiler_options` - Proplist with extra options passed directly to
   `compiler:forms/2`. This option can be supplied multiple times. Note
   `compiler:forms/2`. This option can be supplied multiple times. Note
   that the most common options can be given directly with the list of
   that the most common options can be given directly with the list of
-  options to the erlydtl compiler (see Erlang Compiler options,
+  options to the erlydtl compiler (see options affecting the Erlang
-  below).
+  compiler, below).
 
 
 * `force_recompile` - Recompile the module even if the source's
 * `force_recompile` - Recompile the module even if the source's
   checksum has not changed. Useful for debugging.
   checksum has not changed. Useful for debugging.
@@ -111,8 +123,6 @@ Options is a proplist possibly containing:
 * `binary_strings` - Whether to compile strings as binary terms
 * `binary_strings` - Whether to compile strings as binary terms
   (rather than lists). Defaults to `true`.
   (rather than lists). Defaults to `true`.
 
 
-* `verbose` - Enable verbose printing of compilation results.
-
 * `record_info` - List of records to look for when rendering the
 * `record_info` - List of records to look for when rendering the
   template. Each record info is a tuple with the fields of the record:
   template. Each record info is a tuple with the fields of the record:
 
 
@@ -129,30 +139,26 @@ Options is a proplist possibly containing:
   similar use, except that this option does NOT affect whether or not
   similar use, except that this option does NOT affect whether or not
   a .beam file is saved.
   a .beam file is saved.
 
 
-*Erlang Compiler options*
+*The following options also affect the Erlang Compiler*
-
+
-As a convenience, the following options are forwarded to
+The following options are forwarded to `compile:forms/2`, (in
-`compile:forms/2`, along with those from `compiler_options`:
+addition to those given with the `compiler_options`):
-
+
-* `return`
+* `return` - Short form for both `return_warnings` and `return_errors`.
-* `return_warnings`
+* `return_warnings` - If this flag is set, then an extra field
-* `return_errors`
+  containing warnings is added to the tuple returned on success.
-* `report`
+* `return_errors` - If this flag is set, then an error-tuple with two
-* `report_warnings`
+  extra fields containing errors and warnings is returned when there
-* `report_errors`
+  are errors.
-* `warnings_as_errors`
+* `report` - Short form for both `report_warnings` and `report_errors`.
-* `verbose`
+* `report_warnings` - Print warnings as they occur.
+* `report_errors` - Print errors as they occur.
+* `warnings_as_errors` - Treat warnings as errors.
+* `verbose` - Enable verbose printing of compilation results.
 
 
-_Notice_ that the return value from `erlydtl:compile` is affected by
+See
-the options passed to the compiler. See
 [Erlang compiler documentation](http://www.erlang.org/doc/man/compile.html#forms-2)
 [Erlang compiler documentation](http://www.erlang.org/doc/man/compile.html#forms-2)
-for details. _Exception_ to the rule is that we have reverted the
+for documentation of these options with regard to the beam compiler.
-`forms` statement that `binary` is treated as implicitly set. That is,
-you need to pass the `binary` option to erlydtl in order to get the
-code in the result tuple.
-
-Default compiler options are `[verbose, report_errors]`, which gives
-either a `{ok, Module}` or `error` as return value.
 
 
 
 
 Helper compilation
 Helper compilation

+ 11 - 2
include/erlydtl_ext.hrl

@@ -1,4 +1,10 @@
 
 
+-record(error_info, {
+          return = false,
+          report = false,
+          list = []
+         }).
+
 -record(dtl_context, {
 -record(dtl_context, {
           local_scopes = [], 
           local_scopes = [], 
           block_dict = dict:new(), 
           block_dict = dict:new(), 
@@ -13,7 +19,7 @@
           custom_tags_dir = [],
           custom_tags_dir = [],
           custom_tags_modules = [],
           custom_tags_modules = [],
           reader = {file, read_file},
           reader = {file, read_file},
-          module = [],
+          module = undefined,
           compiler_options = [verbose, report_errors],
           compiler_options = [verbose, report_errors],
           binary_strings = true,
           binary_strings = true,
           force_recompile = false,
           force_recompile = false,
@@ -23,7 +29,10 @@
           extension_module = undefined,
           extension_module = undefined,
           scanner_module = erlydtl_scanner,
           scanner_module = erlydtl_scanner,
           scanned_tokens = [],
           scanned_tokens = [],
-          all_options = []
+          all_options = [],
+          errors = #error_info{},
+          warnings = #error_info{},
+          bin = undefined
          }).
          }).
 
 
 -record(ast_info, {
 -record(ast_info, {

+ 215 - 70
src/erlydtl_compiler.erl

@@ -43,7 +43,8 @@
 %% --------------------------------------------------------------------
 %% --------------------------------------------------------------------
 %% Definitions
 %% Definitions
 %% --------------------------------------------------------------------
 %% --------------------------------------------------------------------
--export([compile/2, compile/3, compile_dir/2, compile_dir/3, parse/1]).
+-export([compile/2, compile/3, compile_dir/2, compile_dir/3,
+         parse/1, format_error/1]).
 
 
 %% exported for use by extension modules
 %% exported for use by extension modules
 -export([
 -export([
@@ -61,33 +62,20 @@ compile(FileOrBinary, Module) ->
     compile(FileOrBinary, Module, []).
     compile(FileOrBinary, Module, []).
 
 
 compile(Binary, Module, Options0) when is_binary(Binary) ->
 compile(Binary, Module, Options0) when is_binary(Binary) ->
-    File = "",
+    Options = process_opts("/<text>", Options0),
-    Options = [{compiler_options, [{source, "/<text>"}]}
+    Context = init_dtl_context("<text>", Module, Options),
-               |process_opts(Options0)],
+    compile(Context#dtl_context{ bin = Binary });
-    Context = init_dtl_context(File, Module, Options),
-    case parse(Binary, Context) of
-        up_to_date -> ok;
-        {ok, DjangoParseTree, CheckSum} ->
-            compile_to_binary(File, DjangoParseTree, Context, CheckSum);
-        Other -> Other
-    end;
 
 
 compile(File, Module, Options0) ->
 compile(File, Module, Options0) ->
-    Options = process_opts(Options0),
+    Options = process_opts(File, Options0),
     Context = init_dtl_context(File, Module, Options),
     Context = init_dtl_context(File, Module, Options),
-    case parse(File, Context) of
+    compile(Context).
-        up_to_date -> ok;
-        {ok, DjangoParseTree, CheckSum} ->
-            compile_to_binary(File, DjangoParseTree, Context, CheckSum);
-        Other -> Other
-    end.
-
 
 
 compile_dir(Dir, Module) ->
 compile_dir(Dir, Module) ->
     compile_dir(Dir, Module, []).
     compile_dir(Dir, Module, []).
 
 
 compile_dir(Dir, Module, Options0) ->
 compile_dir(Dir, Module, Options0) ->
-    Options = process_opts(Options0),
+    Options = process_opts(Dir, Options0),
     Context = init_dtl_context_dir(Dir, Module, Options),
     Context = init_dtl_context_dir(Dir, Module, Options),
     %% Find all files in Dir (recursively), matching the regex (no
     %% Find all files in Dir (recursively), matching the regex (no
     %% files ending in "~").
     %% files ending in "~").
@@ -122,19 +110,41 @@ compile_dir(Dir, Module, Options0) ->
 parse(Data) ->
 parse(Data) ->
     parse(Data, #dtl_context{}).
     parse(Data, #dtl_context{}).
 
 
+format_error(no_outdir) ->
+    "Compiled template not saved (need outdir option)";
+format_error(unexpected_extends_tag) ->
+    "The extends tag must be at the very top of the template";
+format_error(circular_include) ->
+    "Circular file inclusion!";
+format_error({read_file, Error}) ->
+    io_lib:format(
+      "Failed to read file: ~s",
+      [file:format_error(Error)]);
+format_error({read_file, File, Error}) ->
+    io_lib:format(
+      "Failed to include file ~s: ~s",
+      [File, file:format_error(Error)]);
+format_error({write_file, Error}) ->
+    io_lib:format(
+      "Failed to write file: ~s",
+      [file:format_error(Error)]);
+format_error(Other) ->
+    io_lib:format("## Error description for ~p not implemented.", [Other]).
+
 
 
 %%====================================================================
 %%====================================================================
 %% Internal functions
 %% Internal functions
 %%====================================================================
 %%====================================================================
 
 
-process_opts(Options) ->
+process_opts(Source, Options) ->
     Options1 = proplists:normalize(
     Options1 = proplists:normalize(
                  update_defaults(Options),
                  update_defaults(Options),
                  [{aliases, [{out_dir, outdir}]}
                  [{aliases, [{out_dir, outdir}]}
                  ]),
                  ]),
-    process_opts(Options1, []).
+    [{compiler_options, [{source, Source}]}
+     |compiler_opts(Options1, [])].
 
 
-process_opts([CompilerOption|Os], Acc)
+compiler_opts([CompilerOption|Os], Acc)
   when
   when
       CompilerOption =:= return;
       CompilerOption =:= return;
       CompilerOption =:= return_warnings;
       CompilerOption =:= return_warnings;
@@ -145,10 +155,10 @@ process_opts([CompilerOption|Os], Acc)
       CompilerOption =:= warnings_as_errors;
       CompilerOption =:= warnings_as_errors;
       CompilerOption =:= verbose;
       CompilerOption =:= verbose;
       element(1, CompilerOption) =:= outdir ->
       element(1, CompilerOption) =:= outdir ->
-    process_opts(Os, [CompilerOption, {compiler_options, [CompilerOption]}|Acc]);
+    compiler_opts(Os, [CompilerOption, {compiler_options, [CompilerOption]}|Acc]);
-process_opts([O|Os], Acc) ->
+compiler_opts([O|Os], Acc) ->
-    process_opts(Os, [O|Acc]);
+    compiler_opts(Os, [O|Acc]);
-process_opts([], Acc) ->
+compiler_opts([], Acc) ->
     lists:reverse(Acc).
     lists:reverse(Acc).
 
 
 update_defaults(Options) ->
 update_defaults(Options) ->
@@ -189,6 +199,56 @@ maybe_add_env_default_opts(Options) ->
         _ -> Options ++ env_default_opts()
         _ -> Options ++ env_default_opts()
     end.
     end.
 
 
+compile(Context) ->
+    Context1 = do_compile(Context),
+    collect_result(Context1).
+
+collect_result(#dtl_context{
+                  module=Module, 
+                  errors=#error_info{ list=[] },
+                  warnings=Ws }=Context) ->
+    Info = case Ws of
+               #error_info{ return=true, list=Warnings } ->
+                   [pack_error_list(Warnings)];
+               _ ->
+                   []
+           end,
+    Res = case proplists:get_bool(binary, Context#dtl_context.all_options) of
+              true ->
+                  [ok, Module, Context#dtl_context.bin | Info];
+              false ->
+                  [ok, Module | Info]
+          end,
+    list_to_tuple(Res);
+collect_result(#dtl_context{ errors=Es, warnings=Ws }) ->
+    if Es#error_info.return ->
+            {error,
+             pack_error_list(Es#error_info.list),
+             case Ws of
+                 #error_info{ list=L } ->
+                     pack_error_list(L);
+                 _ ->
+                     []
+             end};
+       true -> error
+    end.
+
+do_compile(#dtl_context{ bin=undefined, parse_trail=[File|_] }=Context) ->
+    {M, F} = Context#dtl_context.reader,
+    case catch M:F(File) of
+        {ok, Data} when is_binary(Data) ->
+            do_compile(Context#dtl_context{ bin=Data });
+        {error, Reason} ->
+            add_error({read_file, Reason}, Context)
+    end;
+do_compile(#dtl_context{ bin=Binary }=Context) ->
+    case parse(Binary, Context) of
+        up_to_date -> Context;
+        {ok, DjangoParseTree, CheckSum} ->
+            compile_to_binary(DjangoParseTree, CheckSum, Context);
+        {error, Reason} -> add_error(Reason, Context)
+    end.
+
 compile_multiple_to_binary(Dir, ParserResults, Context) ->
 compile_multiple_to_binary(Dir, ParserResults, Context) ->
     MatchAst = options_match_ast(Context),
     MatchAst = options_match_ast(Context),
     {Functions, {AstInfo, _}}
     {Functions, {AstInfo, _}}
@@ -224,19 +284,24 @@ compile_multiple_to_binary(Dir, ParserResults, Context) ->
     Forms = custom_forms(Dir, Context#dtl_context.module, Functions, AstInfo),
     Forms = custom_forms(Dir, Context#dtl_context.module, Functions, AstInfo),
     compile_forms(Forms, Context).
     compile_forms(Forms, Context).
 
 
-compile_to_binary(File, DjangoParseTree, Context, CheckSum) ->
+compile_to_binary(DjangoParseTree, CheckSum, Context) ->
     try body_ast(DjangoParseTree, Context, init_treewalker(Context)) of
     try body_ast(DjangoParseTree, Context, init_treewalker(Context)) of
         {{BodyAst, BodyInfo}, BodyTreeWalker} ->
         {{BodyAst, BodyInfo}, BodyTreeWalker} ->
             try custom_tags_ast(BodyInfo#ast_info.custom_tags, Context, BodyTreeWalker) of
             try custom_tags_ast(BodyInfo#ast_info.custom_tags, Context, BodyTreeWalker) of
                 {{CustomTagsAst, CustomTagsInfo}, _} ->
                 {{CustomTagsAst, CustomTagsInfo}, _} ->
-                    Forms = forms(File, Context#dtl_context.module, {BodyAst, BodyInfo},
+                    Forms = forms(
-                                  {CustomTagsAst, CustomTagsInfo}, CheckSum, Context, BodyTreeWalker),
+                              Context#dtl_context.module,
+                              {BodyAst, BodyInfo},
+                              {CustomTagsAst, CustomTagsInfo},
+                              CheckSum,
+                              BodyTreeWalker,
+                              Context),
                     compile_forms(Forms, Context)
                     compile_forms(Forms, Context)
             catch
             catch
-                throw:Error -> Error
+                throw:Error -> add_error(Error, Context)
             end
             end
     catch
     catch
-        throw:Error -> Error
+        throw:Error -> add_error(Error, Context)
     end.
     end.
 
 
 compile_forms(Forms, Context) ->
 compile_forms(Forms, Context) ->
@@ -246,50 +311,49 @@ compile_forms(Forms, Context) ->
         Compiled when element(1, Compiled) =:= ok ->
         Compiled when element(1, Compiled) =:= ok ->
             [ok, Module, Bin|Info] = tuple_to_list(Compiled),
             [ok, Module, Bin|Info] = tuple_to_list(Compiled),
             lists:foldl(
             lists:foldl(
-              fun (F, ok) -> F(Module, Bin, Context);
+              fun (F, C) -> F(Module, Bin, C) end,
-                  (_, Res) -> Res
+              Context#dtl_context{ bin=Bin },
-              end,
+              [fun maybe_write/3,
-              ok,
-              [fun maybe_write_binary/3,
                fun maybe_load/3,
                fun maybe_load/3,
-               fun (_, _, _) ->
+               fun (_, _, C) ->
-                       case proplists:get_bool(binary, Context#dtl_context.all_options) of
+                       case Info of
-                           true -> Compiled;
+                           [Ws] when length(Ws) > 0 ->
-                           false -> list_to_tuple([ok, Module|Info])
+                               add_warning({compile_beam, Ws}, C);
+                           _ -> C
                        end
                        end
                end
                end
               ]);
               ]);
-        Err when Err =:= error; element(1, Err) =:= error ->
+        error ->
-            Err
+            add_error(compile_beam, Context);
+        {error, Es, Ws} ->
+            add_error({compile_beam, Es, Ws}, Context)
     end.
     end.
 
 
-maybe_write_binary(Module, Bin, Context) ->
+maybe_write(Module, Bin, Context) ->
     case proplists:get_value(outdir, Context#dtl_context.all_options) of
     case proplists:get_value(outdir, Context#dtl_context.all_options) of
         undefined ->
         undefined ->
-            print("Template module: ~w not saved (no outdir option)\n", [Module], Context);
+            add_warning(no_outdir, Context);
         OutDir ->
         OutDir ->
             BeamFile = filename:join([OutDir, atom_to_list(Module) ++ ".beam"]),
             BeamFile = filename:join([OutDir, atom_to_list(Module) ++ ".beam"]),
             print("Template module: ~w -> ~s\n", [Module, BeamFile], Context),
             print("Template module: ~w -> ~s\n", [Module, BeamFile], Context),
             case file:write_file(BeamFile, Bin) of
             case file:write_file(BeamFile, Bin) of
-                ok -> ok;
+                ok -> Context;
                 {error, Reason} ->
                 {error, Reason} ->
-                    {error, lists:flatten(
+                    add_error({write_file, Reason}, Context)
-                              io_lib:format("Beam generation of '~s' failed: ~p",
-                                            [BeamFile, file:format_error(Reason)]))}
             end
             end
     end.
     end.
 
 
 maybe_load(Module, Bin, Context) ->
 maybe_load(Module, Bin, Context) ->
     case proplists:get_bool(no_load, Context#dtl_context.all_options) of
     case proplists:get_bool(no_load, Context#dtl_context.all_options) of
-        true -> ok;
+        true -> Context;
-        false -> load_code(Module, Bin)
+        false -> load_code(Module, Bin, Context)
     end.
     end.
 
 
-load_code(Module, Bin) ->
+load_code(Module, Bin, Context) ->
     code:purge(Module),
     code:purge(Module),
     case code:load_binary(Module, atom_to_list(Module) ++ ".erl", Bin) of
     case code:load_binary(Module, atom_to_list(Module) ++ ".erl", Bin) of
-        {module, Module} -> ok;
+        {module, Module} -> Context;
-        _ -> {error, lists:concat(["code reload failed: ", Module])}
+        Error -> add_warning({load, Error}, Context)
     end.
     end.
 
 
 dump_forms(Forms, Context) ->
 dump_forms(Forms, Context) ->
@@ -335,13 +399,46 @@ init_context(IsCompilingDir, ParseTrail, DefDir, Module, Options) ->
                  extension_module = proplists:get_value(extension_module, Options, Ctx#dtl_context.extension_module),
                  extension_module = proplists:get_value(extension_module, Options, Ctx#dtl_context.extension_module),
                  scanner_module = proplists:get_value(scanner_module, Options, Ctx#dtl_context.scanner_module),
                  scanner_module = proplists:get_value(scanner_module, Options, Ctx#dtl_context.scanner_module),
                  record_info = [{R, lists:zip(I, lists:seq(2, length(I) + 1))}
                  record_info = [{R, lists:zip(I, lists:seq(2, length(I) + 1))}
-                                || {R, I} <- proplists:get_value(record_info, Options, Ctx#dtl_context.record_info)]
+                                || {R, I} <- proplists:get_value(record_info, Options, Ctx#dtl_context.record_info)],
+                 errors = init_error_info(errors, Ctx#dtl_context.errors, Options),
+                 warnings = init_error_info(warnings, Ctx#dtl_context.warnings, Options)
                 },
                 },
     case call_extension(Context, init_context, [Context]) of
     case call_extension(Context, init_context, [Context]) of
         {ok, C} when is_record(C, dtl_context) -> C;
         {ok, C} when is_record(C, dtl_context) -> C;
         undefined -> Context
         undefined -> Context
     end.
     end.
 
 
+init_error_info(warnings, Ei, Options) ->
+    case proplists:get_bool(warnings_as_errors, Options) of
+        true -> warnings_as_errors;
+        false ->
+            init_error_info(get_error_info_opts(warnings, Options), Ei)
+    end;
+init_error_info(Class, Ei, Options) ->
+    init_error_info(get_error_info_opts(Class, Options), Ei).
+
+init_error_info([{return, true}|Flags], #error_info{ return = false }=Ei) ->
+    init_error_info(Flags, Ei#error_info{ return = true });
+init_error_info([{report, true}|Flags], #error_info{ report = false }=Ei) ->
+    init_error_info(Flags, Ei#error_info{ report = true });
+init_error_info([_|Flags], Ei) ->
+    init_error_info(Flags, Ei);
+init_error_info([], Ei) -> Ei.
+
+get_error_info_opts(Class, Options) ->
+    Flags = case Class of
+                errors ->
+                    [return, report, {return_errors, return}, {report_errors, report}];
+                warnings ->
+                    [return, report, {return_warnings, return}, {report_warnings, report}]
+            end,
+    [begin
+         {Key, Value} = if is_atom(Flag) -> {Flag, Flag};
+                           true -> Flag
+                        end,
+         {Value, proplists:get_bool(Key, Options)}
+     end || Flag <- Flags].
+    
 init_dtl_context(File, Module, Options) when is_list(Module) ->
 init_dtl_context(File, Module, Options) when is_list(Module) ->
     init_dtl_context(File, list_to_atom(Module), Options);
     init_dtl_context(File, list_to_atom(Module), Options);
 init_dtl_context(File, Module, Options) ->
 init_dtl_context(File, Module, Options) ->
@@ -408,16 +505,9 @@ parse(File, Context) ->
     {M, F} = Context#dtl_context.reader,
     {M, F} = Context#dtl_context.reader,
     case catch M:F(File) of
     case catch M:F(File) of
         {ok, Data} when is_binary(Data) ->
         {ok, Data} when is_binary(Data) ->
-            case parse(Data, Context) of
+            parse(Data, Context);
-                {error, Msg} when is_list(Msg) ->
+        {error, Reason} ->
-                    {error, File ++ ": " ++ Msg};
+            {read_file, File, Reason}
-                {error, Msg} ->
-                    {error, {File, [Msg]}};
-                Result ->
-                    Result
-            end;
-        _ ->
-            {error, {File, [{0, Context#dtl_context.module, "Failed to read file"}]}}
     end.
     end.
 
 
 do_parse(Data, #dtl_context{ scanner_module=Scanner }=Context) ->
 do_parse(Data, #dtl_context{ scanner_module=Scanner }=Context) ->
@@ -680,7 +770,8 @@ custom_forms(Dir, Module, Functions, AstInfo) ->
               | FunctionAsts] ++ AstInfo#ast_info.pre_render_asts
               | FunctionAsts] ++ AstInfo#ast_info.pre_render_asts
     ].
     ].
 
 
-forms(File, Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}, CheckSum, Context, TreeWalker) ->
+forms(Module, {BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}, CheckSum, TreeWalker,
+      #dtl_context{ parse_trail=[File|_] }=Context) ->
     MergedInfo = merge_info(BodyInfo, CustomTagsInfo),
     MergedInfo = merge_info(BodyInfo, CustomTagsInfo),
     Render0FunctionAst = erl_syntax:function(
     Render0FunctionAst = erl_syntax:function(
                            erl_syntax:atom(render),
                            erl_syntax:atom(render),
@@ -804,7 +895,7 @@ body_ast([{'extends', {string_literal, _Pos, String}} | ThisParseTree], Context,
     File = full_path(unescape_string_literal(String), Context#dtl_context.doc_root),
     File = full_path(unescape_string_literal(String), Context#dtl_context.doc_root),
     case lists:member(File, Context#dtl_context.parse_trail) of
     case lists:member(File, Context#dtl_context.parse_trail) of
         true ->
         true ->
-            throw({error, "Circular file inclusion!"});
+            throw(circular_include);
         _ ->
         _ ->
             case parse(File, Context) of
             case parse(File, Context) of
                 {ok, ParentParseTree, CheckSum} ->
                 {ok, ParentParseTree, CheckSum} ->
@@ -932,7 +1023,7 @@ body_ast(DjangoParseTree, Context, TreeWalker) ->
                                        ({'extension', Tag}, TreeWalkerAcc) ->
                                        ({'extension', Tag}, TreeWalkerAcc) ->
                                                        extension_ast(Tag, Context, TreeWalkerAcc);
                                                        extension_ast(Tag, Context, TreeWalkerAcc);
                                        ({'extends', _}, _TreeWalkerAcc) ->
                                        ({'extends', _}, _TreeWalkerAcc) ->
-                                                       throw({error, "The extends tag must be at the very top of the template"});
+                                                       throw(unexpected_extends_tag);
                                        (ValueToken, TreeWalkerAcc) ->
                                        (ValueToken, TreeWalkerAcc) ->
                                                        {{ValueAst,ValueInfo},ValueTreeWalker} = value_ast(ValueToken, true, true, Context, TreeWalkerAcc),
                                                        {{ValueAst,ValueInfo},ValueTreeWalker} = value_ast(ValueToken, true, true, Context, TreeWalkerAcc),
                                                        {{format(ValueAst, Context, ValueTreeWalker),ValueInfo},ValueTreeWalker}
                                                        {{format(ValueAst, Context, ValueTreeWalker),ValueInfo},ValueTreeWalker}
@@ -1006,7 +1097,7 @@ value_ast(ValueToken, AsString, EmptyIfUndefined, Context, TreeWalker) ->
 extension_ast(Tag, Context, TreeWalker) ->
 extension_ast(Tag, Context, TreeWalker) ->
     case call_extension(Context, compile_ast, [Tag, Context, TreeWalker]) of
     case call_extension(Context, compile_ast, [Tag, Context, TreeWalker]) of
         undefined ->
         undefined ->
-            throw({error, {unknown_extension, Tag}});
+            throw({unknown_extension, Tag});
         Result ->
         Result ->
             Result
             Result
     end.
     end.
@@ -1274,7 +1365,7 @@ filter_ast2(Name, Args, #dtl_context{ filter_modules = [Module|Rest] } = Context
             filter_ast2(Name, Args, Context#dtl_context{ filter_modules = Rest })
             filter_ast2(Name, Args, Context#dtl_context{ filter_modules = Rest })
     end;
     end;
 filter_ast2(Name, Args, _) ->
 filter_ast2(Name, Args, _) ->
-    throw({error, {unknown_filter, Name, length(Args)}}).
+    throw({unknown_filter, Name, length(Args)}).
 
 
 search_for_escape_filter(Variable, Filter, #dtl_context{auto_escape = on}) ->
 search_for_escape_filter(Variable, Filter, #dtl_context{auto_escape = on}) ->
     search_for_safe_filter(Variable, Filter);
     search_for_safe_filter(Variable, Filter);
@@ -1708,3 +1799,57 @@ call_ast(Module, Variable, AstInfo, TreeWalker) ->
                  [ErrStrAst]),
                  [ErrStrAst]),
     CallAst = erl_syntax:case_expr(AppAst, [OkAst, ErrorAst]),
     CallAst = erl_syntax:case_expr(AppAst, [OkAst, ErrorAst]),
     with_dependencies(Module:dependencies(), {{CallAst, AstInfo}, TreeWalker}).
     with_dependencies(Module:dependencies(), {{CallAst, AstInfo}, TreeWalker}).
+
+
+add_error(Error, #dtl_context{ errors=Es }=Context) ->
+    Context#dtl_context{
+      errors=log_error_info(
+               "", error_info(Error),
+               Es, Context)
+     }.
+
+add_warning(Warning, #dtl_context{ warnings=warnings_as_errors }=Context) ->
+    add_error(Warning, Context);
+add_warning(Warning, #dtl_context{ warnings=Ws }=Context) ->
+    Context#dtl_context{
+      warnings=log_error_info(
+                 "Warning: ",
+                 error_info(Warning),
+                 Ws, Context) 
+     }.
+
+error_info({Line, ErrorDesc}) when is_integer(Line) ->
+    {Line, ?MODULE, ErrorDesc};
+error_info({Line, Module, _ErrorDesc}=ErrorInfo)
+  when is_integer(Line), is_atom(Module) -> ErrorInfo;
+error_info(ErrorDesc) -> {none, ?MODULE, ErrorDesc}.
+
+log_error_info(Prefix, {Line, Module, ErrorDesc}=ErrorInfo,
+               #error_info{ report=Report, list=L }=Ei,
+               #dtl_context{ parse_trail=[File|_] }) ->
+    if Report ->
+            io:format("~s:~s~s~s~n",
+                      [File, line_info(Line), Prefix,
+                       Module:format_error(ErrorDesc)]);
+       true -> nop
+    end,
+    Ei#error_info{ list=[{File, ErrorInfo}|L] }.
+
+line_info(none) -> " ";
+line_info(Line) when is_integer(Line) ->
+    io_lib:format("~b: ", [Line]).
+
+pack_error_list(Es) ->
+    collect_error_info([], Es, []).
+
+collect_error_info([], [], Acc) ->
+    lists:reverse(Acc);
+collect_error_info([{File, ErrorInfo}|Es], Rest, [{File, FEs}|Acc]) ->
+    collect_error_info(Es, Rest, [{File, [ErrorInfo|FEs]}|Acc]);
+collect_error_info([E|Es], Rest, Acc) ->
+    collect_error_info(Es, [E|Rest], Acc);
+collect_error_info([], Rest, Acc) ->
+    case lists:reverse(Rest) of
+        [{File, ErrorInfo}|Es] ->
+            collect_error_info(Es, [], [{File, [ErrorInfo]}|Acc])
+    end.

+ 9 - 5
tests/src/erlydtl_functional_tests.erl

@@ -76,11 +76,14 @@ setup_compile("var_preset") ->
     CompileVars = [{preset_var1, "preset-var1"}, {preset_var2, "preset-var2"}],
     CompileVars = [{preset_var1, "preset-var1"}, {preset_var2, "preset-var2"}],
     {ok, CompileVars};
     {ok, CompileVars};
 setup_compile("extends2") ->
 setup_compile("extends2") ->
-    {{error, "The extends tag must be at the very top of the template"}, []};
+    File = templates_dir("input/extends2"),
+    Error = {none, erlydtl_compiler, unexpected_extends_tag},
+    {{error, [{File, [Error]}], []}, []};
 setup_compile("extends3") ->
 setup_compile("extends3") ->
-    File = templates_dir("input/imaginary"),
+    File = templates_dir("input/extends3"),
-    Error = {0, functional_test_extends3, "Failed to read file"},
+    Include = templates_dir("input/imaginary"),
-    {{error, {File, [Error]}}, []}; %% Huh?! what kind of error message is that!?
+    Error = {none, erlydtl_compiler, {read_file, Include, enoent}},
+    {{error, [{File, [Error]}], []}, []};
 setup_compile(_) ->
 setup_compile(_) ->
     {ok, []}.
     {ok, []}.
 
 
@@ -249,6 +252,7 @@ test_compile_render(Name) ->
             Options = [
             Options = [
                        {vars, CompileVars},
                        {vars, CompileVars},
                        force_recompile,
                        force_recompile,
+                       return_errors,
                        %% debug_compiler,
                        %% debug_compiler,
                        {custom_tags_modules, [erlydtl_custom_tags]}],
                        {custom_tags_modules, [erlydtl_custom_tags]}],
             io:format("compiling ... "),
             io:format("compiling ... "),
@@ -259,7 +263,7 @@ test_compile_render(Name) ->
                             io:format("missing error"),
                             io:format("missing error"),
                             {error, "compiling should have failed :" ++ File}
                             {error, "compiling should have failed :" ++ File}
                     end;
                     end;
-                {error, _}=Err ->
+                {error, _, _}=Err ->
                     if CompileStatus =:= Err -> io:format("ok");
                     if CompileStatus =:= Err -> io:format("ok");
                        true ->
                        true ->
                             io:format("failed"),
                             io:format("failed"),

+ 2 - 1
tests/src/erlydtl_unittests.erl

@@ -1220,7 +1220,7 @@ tests() ->
          [{extension_module, erlydtl_extension_test}], <<"ok">>},
          [{extension_module, erlydtl_extension_test}], <<"ok">>},
         {"proper error message", <<"{{ bar # }}">>, [{bar, "ok"}], [],
         {"proper error message", <<"{{ bar # }}">>, [{bar, "ok"}], [],
          [{extension_module, erlydtl_extension_test}],
          [{extension_module, erlydtl_extension_test}],
-         {error, {1,erlydtl_extension_test,"Unexpected '#' in code at column 8"}}},
+         {error, [{"<text>", [{1,erlydtl_extension_test,"Unexpected '#' in code at column 8"}]}], []}},
         %% accept identifiers as expressions (this is a dummy functionality to test the parser extensibility)
         %% accept identifiers as expressions (this is a dummy functionality to test the parser extensibility)
         {"identifiers as expressions", <<"{{ foo.bar or baz }}">>, [{baz, "ok"}], [],
         {"identifiers as expressions", <<"{{ foo.bar or baz }}">>, [{baz, "ok"}], [],
          [{extension_module, erlydtl_extension_test}], <<"ok">>}
          [{extension_module, erlydtl_extension_test}], <<"ok">>}
@@ -1304,6 +1304,7 @@ format_error(Name, Class, Error) ->
 
 
 compile_test(DTL, Opts) ->
 compile_test(DTL, Opts) ->
     Options = [force_recompile,
     Options = [force_recompile,
+               return_errors,
                {custom_filters_modules, [erlydtl_contrib_humanize]}
                {custom_filters_modules, [erlydtl_contrib_humanize]}
                |Opts],
                |Opts],
     timer:tc(erlydtl, compile, [DTL, erlydtl_running_test, Options]).
     timer:tc(erlydtl, compile, [DTL, erlydtl_running_test, Options]).