Skip to content

Commit 254ba1c

Browse files
Move template expansion to "format-time"
This moves the expansion of templates from macro expansion time to later on. This allows us to avoid having to search through expressions looking for the actual expression type since those may be hidden by "decorator" macros. It also handles cases where `@doc` is used on a string macro docstring, such as @doc raw"..." f(x) = x Implementation is based on a new internal abbreviation type called `Template` which gets prepended and appended to docstrings in modules where templates are available. Then, during formatting, we expand these `Template` abbreviations into there definitions. Fixes #73.
1 parent 1594674 commit 254ba1c

File tree

5 files changed

+93
-38
lines changed

5 files changed

+93
-38
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
docs/build
55
docs/site
66
docs/Manifest.toml
7+
Manifest.toml

src/abbreviations.jl

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,3 +611,63 @@ of the docstring body that should be spliced into a template.
611611
const DOCSTRING = DocStringTemplate()
612612

613613
# NOTE: no `format` needed for this 'mock' abbreviation.
614+
615+
is_docstr_template(::DocStringTemplate) = true
616+
is_docstr_template(other) = false
617+
618+
"""
619+
Internal abbreviation type used to wrap templated docstrings.
620+
621+
`Location` is a `Symbol`, either `:before` or `:after`. `dict` stores a
622+
reference to a module's templates.
623+
"""
624+
struct Template{Location} <: Abbreviation
625+
dict::Dict{Symbol,Vector{Any}}
626+
end
627+
628+
function format(abbr::Template, buf, doc)
629+
# Find the applicable template based on the kind of docstr.
630+
parts = get_template(abbr.dict, template_key(doc))
631+
# Replace the abbreviation with either the parts of the template found
632+
# before the `DOCSTRING` abbreviation, or after it. When no `DOCSTRING`
633+
# exists in the template, which shouldn't really happen then nothing will
634+
# get included here.
635+
for index in included_range(abbr, parts)
636+
# We don't call `DocStringExtensions.format` here since we need to be
637+
# able to format any content in docstrings, rather than just
638+
# abbreviations.
639+
Docs.formatdoc(buf, doc, parts[index])
640+
end
641+
end
642+
643+
function included_range(abbr::Template, parts::Vector)
644+
# Select the correct indexing depending on what we find.
645+
build_range(::Template, ::Nothing) = 0:-1
646+
build_range(::Template{:before}, index) = 1:(index - 1)
647+
build_range(::Template{:after}, index) = (index + 1):lastindex(parts)
648+
# Search for index from either the front or back.
649+
find_index(::Template{:before}) = findfirst(is_docstr_template, parts)
650+
find_index(::Template{:after}) = findlast(is_docstr_template, parts)
651+
# Find and return the correct indices.
652+
return build_range(abbr, find_index(abbr))
653+
end
654+
655+
function template_key(doc::Docs.DocStr)
656+
# Local helper methods for extracting the template key from a docstring.
657+
ismacro(b::Docs.Binding) = startswith(string(b.var), '@')
658+
objname(obj::Union{Function,Module,DataType,UnionAll,Core.IntrinsicFunction}, b::Docs.Binding) = nameof(obj)
659+
objname(obj, b::Docs.Binding) = Symbol("") # Empty to force resolving to `:CONSTANTS` below.
660+
# Select the key returned based on input argument types.
661+
_key(::Module, sig, binding) = :MODULES
662+
_key(::Function, ::typeof(Union{}), binding) = ismacro(binding) ? :MACROS : :FUNCTIONS
663+
_key(::Function, sig, binding) = ismacro(binding) ? :MACROS : :METHODS
664+
_key(::DataType, ::typeof(Union{}), binding) = :TYPES
665+
_key(::DataType, sig, binding) = :METHODS
666+
_key(other, sig, binding) = :DEFAULT
667+
668+
binding = doc.data[:binding]
669+
obj = Docs.resolve(binding)
670+
name = objname(obj, binding)
671+
key = name === binding.var ? _key(obj, doc.data[:typesig], binding) : :CONSTANTS
672+
return key
673+
end

src/templates.jl

Lines changed: 14 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -102,18 +102,21 @@ end
102102
# On v0.6 and below it seems it was assumed to be (docstr::String, expr::Expr), but on v0.7
103103
# it is (source::LineNumberNode, mod::Module, docstr::String, expr::Expr)
104104
function template_hook(source::LineNumberNode, mod::Module, docstr, expr::Expr)
105-
local docex = interp_string(docstr)
106-
if isdefined(mod, TEMP_SYM) && Meta.isexpr(docex, :string)
107-
local templates = getfield(mod, TEMP_SYM)
108-
local template = get_template(templates, expression_type(expr))
109-
local out = Expr(:string)
110-
for t in template
111-
t == DOCSTRING ? append!(out.args, docex.args) : push!(out.args, t)
112-
end
113-
return (source, mod, out, expr)
114-
else
115-
return (source, mod, docstr, expr)
105+
# During macro expansion we only need to wrap docstrings in special
106+
# abbreviations that later print out what was before and after the
107+
# docstring in it's specific template. This is only done when the module
108+
# actually defines templates.
109+
if isdefined(mod, TEMP_SYM)
110+
dict = getfield(mod, TEMP_SYM)
111+
# We unwrap interpolated strings so that we can add the `:before` and
112+
# `:after` abbreviations. Otherwise they're just left as is.
113+
unwrapped = Meta.isexpr(docstr, :string) ? docstr.args : [docstr]
114+
before, after = Template{:before}(dict), Template{:after}(dict)
115+
# Rebuild the original docstring, but with the template abbreviations
116+
# surrounding it.
117+
docstr = Expr(:string, before, unwrapped..., after)
116118
end
119+
return (source, mod, docstr, expr)
117120
end
118121

119122
function template_hook(docstr, expr::Expr)
@@ -123,29 +126,4 @@ end
123126

124127
template_hook(args...) = args
125128

126-
interp_string(str::AbstractString) = Expr(:string, str)
127-
interp_string(other) = other
128-
129129
get_template(t::Dict, k::Symbol) = haskey(t, k) ? t[k] : get(t, :DEFAULT, Any[DOCSTRING])
130-
131-
function expression_type(ex::Expr)
132-
# Expression heads changed in JuliaLang/julia/pull/23157 to match the new keyword syntax.
133-
if VERSION < v"0.7.0-DEV.1263" && Meta.isexpr(ex, [:type, :bitstype])
134-
:TYPES
135-
elseif Meta.isexpr(ex, :module)
136-
:MODULES
137-
elseif Meta.isexpr(ex, [:struct, :abstract, :typealias, :primitive])
138-
:TYPES
139-
elseif Meta.isexpr(ex, :macro)
140-
:MACROS
141-
elseif Meta.isexpr(ex, [:function, :(=)]) && Meta.isexpr(ex.args[1], :call) || (Meta.isexpr(ex.args[1], :where) && Meta.isexpr(ex.args[1].args[1], :call))
142-
:METHODS
143-
elseif Meta.isexpr(ex, :function)
144-
:FUNCTIONS
145-
elseif Meta.isexpr(ex, [:const, :(=)])
146-
:CONSTANTS
147-
else
148-
:DEFAULT
149-
end
150-
end
151-
expression_type(other) = :DEFAULT

test/templates.jl

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,18 @@ const K = 1
3535
"mutable struct `T`"
3636
mutable struct T end
3737

38+
"`@kwdef` struct `S`"
39+
Base.@kwdef struct S end
40+
3841
"method `f`"
3942
f(x) = x
4043

4144
"method `g`"
4245
g(::Type{T}) where {T} = T # Issue 32
4346

47+
"inlined method `h`"
48+
@inline h(x) = x
49+
4450
"macro `@m`"
4551
macro m(x) end
4652

@@ -66,8 +72,15 @@ module InnerModule
6672
"constant `K`"
6773
const K = 1
6874

69-
"mutable struct `T`"
70-
mutable struct T end
75+
"""
76+
mutable struct `T`
77+
78+
$(FIELDS)
79+
"""
80+
mutable struct T
81+
"field docs for x"
82+
x
83+
end
7184

7285
"method `f`"
7386
f(x) = x

test/tests.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,12 +423,15 @@ end
423423
let fmt = expr -> Markdown.plain(eval(:(@doc $expr)))
424424
@test occursin("(DEFAULT)", fmt(:(TemplateTests.K)))
425425
@test occursin("(TYPES)", fmt(:(TemplateTests.T)))
426+
@test occursin("(TYPES)", fmt(:(TemplateTests.S)))
426427
@test occursin("(METHODS, MACROS)", fmt(:(TemplateTests.f)))
427428
@test occursin("(METHODS, MACROS)", fmt(:(TemplateTests.g)))
429+
@test occursin("(METHODS, MACROS)", fmt(:(TemplateTests.h)))
428430
@test occursin("(METHODS, MACROS)", fmt(:(TemplateTests.@m)))
429431

430432
@test occursin("(DEFAULT)", fmt(:(TemplateTests.InnerModule.K)))
431433
@test occursin("(DEFAULT)", fmt(:(TemplateTests.InnerModule.T)))
434+
@test occursin("field docs for x", fmt(:(TemplateTests.InnerModule.T)))
432435
@test occursin("(METHODS, MACROS)", fmt(:(TemplateTests.InnerModule.f)))
433436
@test occursin("(MACROS)", fmt(:(TemplateTests.InnerModule.@m)))
434437

0 commit comments

Comments
 (0)