From 2f8c17ba33ab230992d0b1ecd892b9ecee3b074d Mon Sep 17 00:00:00 2001 From: Michael Hatherly Date: Sun, 26 Nov 2023 20:34:56 +0000 Subject: [PATCH] Correct file and line info in generated code (#15) Switch out macro-generated file and line info in template functions with the correct locations based on the parsed template code. Makes stacktraces to errors in template functions point to the correct lines in the right files instead of internal package code. --- src/HypertextTemplates.jl | 5 ++ src/custom_element.jl | 2 +- src/nodes/abstract.jl | 4 +- src/nodes/element.jl | 8 +-- src/nodes/for.jl | 8 ++- src/nodes/function.jl | 9 ++- src/nodes/julia.jl | 9 +-- src/nodes/match.jl | 12 ++-- src/nodes/show.jl | 9 +-- src/nodes/slot.jl | 11 +-- src/nodes/text.jl | 9 +-- src/utilities.jl | 21 ++++++ test/runtests.jl | 70 ++++++++++++++++++++ test/templates.jl | 1 + test/templates/basic/file-and-line-info.html | 31 +++++++++ 15 files changed, 174 insertions(+), 35 deletions(-) create mode 100644 test/templates/basic/file-and-line-info.html diff --git a/src/HypertextTemplates.jl b/src/HypertextTemplates.jl index c486233..74124e3 100644 --- a/src/HypertextTemplates.jl +++ b/src/HypertextTemplates.jl @@ -18,6 +18,11 @@ end export @template_str, @custom_element, @create_template_str, render, TemplateFileLookup +# Constants. + +# Used for replacing package-specific file/line information in macro-generated code. +const SRC_DIR = @__DIR__ + # Includes. include("utilities.jl") diff --git a/src/custom_element.jl b/src/custom_element.jl index fcdb090..5413f9a 100644 --- a/src/custom_element.jl +++ b/src/custom_element.jl @@ -25,7 +25,7 @@ macro custom_element(name) attributes..., ) end - end + end |> lln_replacer(file, line) end function custom_element(io::IO, name::String, slots::NamedTuple; attributes...) diff --git a/src/nodes/abstract.jl b/src/nodes/abstract.jl index 921484a..f2a832e 100644 --- a/src/nodes/abstract.jl +++ b/src/nodes/abstract.jl @@ -24,7 +24,7 @@ function transform(n::EzXML.Node) return Element(n) end else - return Text(cdata(n)) + return Text(cdata(n), 0) end end end @@ -32,7 +32,7 @@ end function nodeline(node::EzXML.Node) node_ptr = node.ptr @assert node_ptr != C_NULL - @assert unsafe_load(node_ptr).typ == EzXML.ELEMENT_NODE + @assert unsafe_load(node_ptr).typ in (EzXML.ELEMENT_NODE, EzXML.TEXT_NODE) return unsafe_load(convert(Ptr{EzXML._Element}, node_ptr)).line end diff --git a/src/nodes/element.jl b/src/nodes/element.jl index 7247842..bec7d83 100644 --- a/src/nodes/element.jl +++ b/src/nodes/element.jl @@ -108,7 +108,7 @@ function expression(c::BuilderContext, e::Element) $(attrs...), ) print($(c.io), "/>") - end + end |> lln_replacer(c.file, e.line) else name = Symbol(e.name) body = expression(c, e.body) @@ -122,7 +122,7 @@ function expression(c::BuilderContext, e::Element) print($(c.io), ">") $(body) print($(c.io), "") - end + end |> lln_replacer(c.file, e.line) end else name = Symbol(e.name) @@ -132,7 +132,7 @@ function expression(c::BuilderContext, e::Element) $(body) return nothing end - end) + end) |> lln_replacer(c.file, e.line) end slots = Expr( :tuple, @@ -141,7 +141,7 @@ function expression(c::BuilderContext, e::Element) (builder(Symbol(k), expression(c, v)) for (k, v) in e.slots)..., ), ) - :($(name)($(c.io), $(slots); $(attrs...))) + :($(name)($(c.io), $(slots); $(attrs...))) |> lln_replacer(c.file, e.line) end end diff --git a/src/nodes/for.jl b/src/nodes/for.jl index bd8d338..49768ec 100644 --- a/src/nodes/for.jl +++ b/src/nodes/for.jl @@ -5,13 +5,15 @@ struct For <: AbstractNode item::String index::Union{String,Nothing} body::Vector{AbstractNode} + line::Int - function For(iter, item, index, body) + function For(iter, item, index, body, line) return new( _restore_special_symbols(iter), _restore_special_symbols(item), _restore_special_symbols(index), body, + line, ) end end @@ -25,7 +27,7 @@ function For(n::EzXML.Node) iter = key_default(attrs, "iter") item = key_default(attrs, "item") index = key_default(attrs, "index") - return For(iter, item, index, transform(EzXML.nodes(n))) + return For(iter, item, index, transform(EzXML.nodes(n)), nodeline(n)) else error("expected a '' tag, found: $tag") end @@ -46,5 +48,5 @@ function expression(c::BuilderContext, f::For) for $(item) in $(iter) $(body) end - end + end |> lln_replacer(c.file, f.line) end diff --git a/src/nodes/function.jl b/src/nodes/function.jl index b3ce6d0..5907d79 100644 --- a/src/nodes/function.jl +++ b/src/nodes/function.jl @@ -236,8 +236,9 @@ struct TemplateFunction body::Vector{AbstractNode} file::String mod::Module + line::Int - function TemplateFunction(name, html, props, body, file, mod) + function TemplateFunction(name, html, props, body, file, mod, line) if name in VALID_HTML_ELEMENTS error( "cannot name a template function the same as a valid HTML element name: $name", @@ -253,7 +254,7 @@ struct TemplateFunction "cannot name a template function the same as a reserved element name: $name", ) end - return new(_restore_special_symbols(name), html, props, body, file, mod) + return new(_restore_special_symbols(name), html, props, body, file, mod, line) end end @@ -273,6 +274,7 @@ function TemplateFunction(n::EzXML.Node, file::String, mod::Module) transform(EzXML.nodes(n)), file, mod, + nodeline(n), ) else error( @@ -291,6 +293,7 @@ function TemplateFunction(n::EzXML.Node, file::String, mod::Module) transform(EzXML.nodes(n)), file, mod, + nodeline(n), ) else error("expected a '' or '' tag, found: $tag") @@ -396,7 +399,7 @@ function expression(c::TemplateFunction)::Expr end end end - return Base.remove_linenums!(expr) + return expr |> lln_replacer(c.file, c.line) end # Decide whether to recompile the template or not. diff --git a/src/nodes/julia.jl b/src/nodes/julia.jl index 9d20c36..30b7f9c 100644 --- a/src/nodes/julia.jl +++ b/src/nodes/julia.jl @@ -2,9 +2,10 @@ const JULIA_TAG = "julia" struct Julia <: AbstractNode value::String + line::Int - function Julia(value) - return new(_restore_special_symbols(value)) + function Julia(value, line) + return new(_restore_special_symbols(value), line) end end @@ -13,7 +14,7 @@ function Julia(n::EzXML.Node) if length(attrs) == 1 (name, value), = attrs if name == "value" - return Julia(isempty(value) ? name : value) + return Julia(isempty(value) ? name : value, nodeline(n)) else error("expected a 'value' attribute for a julia node.") end @@ -26,7 +27,7 @@ function expression(c::BuilderContext, j::Julia) expr = Meta.parse(j.value) quote $(escape_html)($(c.io), $(expr)) - end + end |> lln_replacer(c.file, j.line) end function escape_html(io::IO, value) diff --git a/src/nodes/match.jl b/src/nodes/match.jl index 1d45e0c..b05974d 100644 --- a/src/nodes/match.jl +++ b/src/nodes/match.jl @@ -3,9 +3,10 @@ const CASE_TAG = "case" struct Case when::String body::Vector{AbstractNode} + line::Int - function Case(when, body) - return new(_restore_special_symbols(when), body) + function Case(when, body, line) + return new(_restore_special_symbols(when), body, line) end end @@ -17,6 +18,7 @@ const MATCH_TAG = "match" struct Match <: AbstractNode value::String cases::Vector{Case} + line::Int function Match(n::EzXML.Node) tag = EzXML.nodename(n) @@ -31,7 +33,7 @@ struct Match <: AbstractNode if length(attrs) == 1 when = key_default(attrs, "when") body = transform(EzXML.nodes(each)) - push!(cases, Case(when, body)) + push!(cases, Case(when, body, nodeline(each))) else error("'match' nodes require a single attribute.") end @@ -42,7 +44,7 @@ struct Match <: AbstractNode # Silently drops text nodes found in the match block. end end - return new(value, cases) + return new(value, cases, nodeline(n)) else error("expected a '' tag, found: $tag") end @@ -64,5 +66,5 @@ function expression(c::BuilderContext, s::Match) $(Deps.Match).@match $(value) begin $(cases...) end - end + end |> lln_replacer(c.file, s.line) end diff --git a/src/nodes/show.jl b/src/nodes/show.jl index 3533ef8..21685a5 100644 --- a/src/nodes/show.jl +++ b/src/nodes/show.jl @@ -4,9 +4,10 @@ struct Show <: AbstractNode when::String body::Vector{AbstractNode} fallback::Vector{AbstractNode} + line::Int - function Show(when, body, fallback) - return new(_restore_special_symbols(when), body, fallback) + function Show(when, body, fallback, line) + return new(_restore_special_symbols(when), body, fallback, line) end end @@ -17,7 +18,7 @@ function Show(n::EzXML.Node) haskey(attrs, "when") || error("expected a 'when' attribute for a 'show' node.") when = key_default(attrs, "when") nodes, fallback = split_fallback(n) - return Show(when, transform(nodes), transform(EzXML.nodes(fallback))) + return Show(when, transform(nodes), transform(EzXML.nodes(fallback)), nodeline(n)) else error("expected a '' tag, found: $tag") end @@ -36,5 +37,5 @@ function expression(c::BuilderContext, s::Show) else $(fallback) end - end + end |> lln_replacer(c.file, s.line) end diff --git a/src/nodes/slot.jl b/src/nodes/slot.jl index 6bef28f..7823c60 100644 --- a/src/nodes/slot.jl +++ b/src/nodes/slot.jl @@ -3,21 +3,22 @@ const UNNAMED_SLOT = "#unnamed_slot#" struct Slot <: AbstractNode name::Union{String,Nothing} + line::Int - function Slot(name) - return new(_restore_special_symbols(name)) + function Slot(name, line) + return new(_restore_special_symbols(name), line) end end function Slot(n::EzXML.Node) attrs = attributes(n) if isempty(attrs) - return Slot(nothing) + return Slot(nothing, nodeline(n)) else if length(attrs) == 1 (name, value), attrs... = attrs if isempty(value) - return Slot(name) + return Slot(name, nodeline(n)) else error("slot name attributes should be valueless, got: $value.") end @@ -33,5 +34,5 @@ function expression(c::BuilderContext, s::Slot) else name = Symbol(s.name) :($(c.slots).$(name)($(c.io))) - end + end |> lln_replacer(c.file, s.line) end diff --git a/src/nodes/text.jl b/src/nodes/text.jl index 84b6a57..b601942 100644 --- a/src/nodes/text.jl +++ b/src/nodes/text.jl @@ -1,8 +1,9 @@ struct Text <: AbstractNode content::String + line::Int - function Text(content::String) - return new(_restore_special_symbols(content)) + function Text(content::String, line) + return new(_restore_special_symbols(content), line) end end @@ -16,7 +17,7 @@ function Text(n::EzXML.Node) right = rstrip(content) content = right == content ? content : "$right " - return Text(all(isspace, content) ? "" : content) + return Text(all(isspace, content) ? "" : content, nodeline(n)) else error("expected a text node, found: $(EzXML.nodename(n))") end @@ -26,6 +27,6 @@ function expression(c::BuilderContext, t::Text) if isempty(t.content) return nothing else - :(print($(c.io), $(t.content))) + :(print($(c.io), $(t.content))) |> lln_replacer(c.file, t.line) end end diff --git a/src/utilities.jl b/src/utilities.jl index 59dfa89..f678928 100644 --- a/src/utilities.jl +++ b/src/utilities.jl @@ -95,3 +95,24 @@ function split_fallback(n::EzXML.Node) end return nodes, fallback end + +function lln_replacer(file::Union{String,Symbol}, line::Integer) + file = Symbol(file) + function (ex::Expr) + if line > 0 + MacroTools.postwalk(ex) do each + if isa(each, LineNumberNode) + if startswith(string(each.file), SRC_DIR) + return LineNumberNode(line, file) + else + return each + end + else + return each + end + end + else + return ex + end + end +end diff --git a/test/runtests.jl b/test/runtests.jl index 4cda179..9adbb83 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -17,6 +17,41 @@ function render(f, args...; kws...) return String(take!(buffer)) end +function check_backtrace(bt, checks) + io = IOBuffer() + Base.show_backtrace(io, bt) + text = String(take!(io)) + function handle(needle::Union{Regex,AbstractString}) + result = contains(text, needle) + if !result + @error "expected to find '$(repr(needle))' in backtrace:\n$text" + end + return result + end + function handle(fn) + result = fn(text) + if !result + @error "expected to find match in backtrace:\n$text" + end + return result + end + for each in vec(checks) + @test handle(each) + end +end + +macro test_throws_st(E, ex, contains) + quote + try + $ex + @test false + catch e + @test isa(e, $E) + check_backtrace(catch_backtrace(), $contains) + end + end +end + @testset "HypertextTemplates" begin HypertextTemplates._DATA_FILENAME_ATTR[] = false @@ -162,6 +197,41 @@ end a = 2, b = 10, ) + + @test_throws_st UndefVarError render(Templates.var"file-and-line-info-1") [ + "file-and-line-info.html:2", + "file-and-line-info.html:1", + !contains("file-and-line-info.html:7"), + !contains(HypertextTemplates.SRC_DIR), + ] + @test_throws_st UndefVarError render(Templates.var"file-and-line-info-2") [ + "file-and-line-info.html:7", + "file-and-line-info.html:5", + !contains("file-and-line-info.html:1"), + !contains(HypertextTemplates.SRC_DIR), + ] + @test_throws_st UndefVarError render(Templates.var"file-and-line-info-3") [ + "file-and-line-info.html:17", + "file-and-line-info.html:15", + !contains("file-and-line-info.html:21"), + !contains(HypertextTemplates.SRC_DIR), + ] + @test_throws_st UndefVarError render(Templates.var"file-and-line-info-4") [ + "file-and-line-info.html:21", + "file-and-line-info.html:23", + "file-and-line-info.html:7", + "file-and-line-info.html:5", + !contains("file-and-line-info.html:1"), + !contains(HypertextTemplates.SRC_DIR), + ] + @test_throws_st UndefVarError render(Templates.var"file-and-line-info-5") [ + "file-and-line-info.html:27", + "file-and-line-info.html:29", + "file-and-line-info.html:15", + "file-and-line-info.html:17", + !contains("file-and-line-info.html:3"), + !contains(HypertextTemplates.SRC_DIR), + ] end @testset "complex" begin diff --git a/test/templates.jl b/test/templates.jl index 5353e5b..b7beea4 100644 --- a/test/templates.jl +++ b/test/templates.jl @@ -15,6 +15,7 @@ template"templates/basic/layout-usage.html" template"templates/basic/special-symbols.html" template"templates/basic/custom-elements.html" template"templates/basic/splat.html" +template"templates/basic/file-and-line-info.html" module Complex diff --git a/test/templates/basic/file-and-line-info.html b/test/templates/basic/file-and-line-info.html new file mode 100644 index 0000000..aa06ca4 --- /dev/null +++ b/test/templates/basic/file-and-line-info.html @@ -0,0 +1,31 @@ + + + + + +
+ + +
Value is unknown.
+
+
+
+
+ + +
+ +
+
+ + +
+ +
+
+ + +
+ +
+
\ No newline at end of file