Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ DocStringExtensions adds abbreviations and templates to Julia's docsystem. An ab
- CI runs Julia 1.0, the LTS, the latest release, and nightly. Code in `src/` and `test/` must parse and run on 1.0, so check any syntax or Base function newer than that against the floor.
- The package leans on undocumented Base internals (`Base.kwarg_decl`, method table fields, binding partitions on 1.12+). The `Base assumptions` testset in `test/base_assumptions.jl` records each one. Add a case there when the code starts relying on another.
- A user-facing change updates every page that shows the feature, in the same commit as the code. Check the docstring, the `Examples` module in `docs/Examples/src/` and its page in `docs/src/examples/`, `docs/src/setup.md`, and `CHANGELOG.md`. Search `docs/` for the names the change touches, then build the docs as CI does. The build runs the doctests and fails on a broken `@ref`.
- From Julia 1.6 the package runs interpreted (`@compiler_options compile = min` in `src/DocStringExtensions.jl`), and it ships no precompile workload, which would slow every dependent's load. Interpreted code costs nothing to compile, but a closure or generator handed to Base, as in `map`, `filter`, `any`, `sort!` with `by` or a comprehension, compiles a new specialization of the Base function on the first render. Write those as loops.
- `format` methods can run in a stale world during macro expansion. Wrap binding lookups such as `Docs.resolve` and `methods` in `Base.invokelatest`, as the existing `format` methods do.

## Running tests
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Changed

- Remove the precompilation workload and the PrecompileTools dependency, which made every package depending on DocStringExtensions slower to precompile and load. On Julia 1.6 and later the package runs interpreted, with inference on. Rendering the first docstring of a session takes about 0.2 s, against 1 s in v0.10.0 ([#313](https://github.com/JuliaDocs/DocStringExtensions.jl/issues/313), [#314](https://github.com/JuliaDocs/DocStringExtensions.jl/pull/314))

## [v0.10.1](https://github.com/JuliaDocs/DocStringExtensions.jl/releases/tag/v0.10.1) - 2026-10-20

### Changed
Expand Down
4 changes: 0 additions & 4 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@ name = "DocStringExtensions"
uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
version = "0.10.1"

[deps]
PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a"

[compat]
DeepDiffs = "1"
PrecompileTools = "1"
julia = "1"

[extras]
Expand Down
1 change: 0 additions & 1 deletion docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ const TOPICS = ["functions", "types", "enums", "modules", "templates"]
makedocs(
sitename = "DocStringExtensions.jl",
modules = [DocStringExtensions, Examples],
checkdocs_ignored_modules = [DocStringExtensions._PrecompileWorkload],
clean = false,
pages = Any[
"Home" => "index.md",
Expand Down
7 changes: 4 additions & 3 deletions src/DocStringExtensions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ $(EXPORTS)
"""
module DocStringExtensions

using PrecompileTools: @setup_workload, @compile_workload
# A docstring renders once per session, so interpreting the package costs less than compiling it.
@static if VERSION >= v"1.6"
Base.Experimental.@compiler_options compile = min
end

# Exports.

Expand Down Expand Up @@ -97,8 +100,6 @@ let λ = s -> isa(s, Symbol) ? getfield(DocStringExtensions, s) : s
end
end

include("precompile.jl")

__init__() = (setter!(template_hook); nothing)

end # module
5 changes: 4 additions & 1 deletion src/abbreviations/fields.jl
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,10 @@ first_line(text) = occursin('\n', text) ? "$(first(split(text, '\n'))) …" : te
# Julia's Markdown reads an even run of backticks as LaTeX, so the fence is the shortest odd
# run longer than any in `text`. A space keeps a backtick at either end out of the fence.
function code_span(text)
longest = reduce(max, [length(m.match) for m in eachmatch(r"`+", text)]; init = 0)
longest = 0
for m in eachmatch(r"`+", text)
longest = max(longest, length(m.match))
end
fence = "`"^(isodd(longest) ? longest + 2 : longest + 1)
padding = startswith(text, '`') || endswith(text, '`') ? " " : ""
return "$fence$padding$text$padding$fence"
Expand Down
10 changes: 8 additions & 2 deletions src/abbreviations/imports.jl
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,19 @@ julia> @doc Plots
"""
const IMPORTS = ModuleImports()

# Kept out of `format`: Julia compiles any method holding a `ccall`, even in an interpreted module.
module_usings(mod::Module) = ccall(:jl_module_usings, Any, (Any,), mod)::Vector{Any}

function format(::ModuleImports, buf, doc)
binding = doc_binding(doc)
object = Base.invokelatest(Docs.resolve, binding)::Module
imports = unique(Vector{Module}(ccall(:jl_module_usings, Any, (Any,), object)::Vector{Any}))
imports = Module[]
for mod in module_usings(object)
mod in imports || push!(imports, mod)
end
return if !isempty(imports)
println(buf)
for mod in sort(imports, by = string)
for mod in insertion_sort!(imports, (a, b) -> string(a) < string(b))
println(buf, " - `", mod, "`")
end
println(buf)
Expand Down
12 changes: 9 additions & 3 deletions src/abbreviations/instances.jl
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ julia> @doc Fruit
const INSTANCES = EnumInstances()

function format(::EnumInstances, buf, doc)
entries = Base.invokelatest(enum_entries ∘ Docs.resolve, doc_binding(doc))::Vector{Tuple{String, String}}
enum = Base.invokelatest(Docs.resolve, doc_binding(doc))
entries = Base.invokelatest(enum_entries, enum)::Vector{Tuple{String, String}}
if !isempty(entries)
println(buf)
for (name, value) in entries
Expand All @@ -39,8 +40,13 @@ function format(::EnumInstances, buf, doc)
return nothing
end

enum_entries(enum::Type{<:Base.Enum}, prefix = "") =
[("$prefix$(Symbol(instance))", string(Integer(instance))) for instance in instances(enum)]
function enum_entries(enum::Type{<:Base.Enum}, prefix = "")
entries = Tuple{String, String}[]
for instance in instances(enum)
push!(entries, ("$prefix$(Symbol(instance))", string(Integer(instance))))
end
return entries
end
# EnumX.jl documents the module that holds its enum type `T`, and callers write `Module.Name`.
enum_entries(mod::Module) = isdefined(mod, :T) ? enum_entries(mod.T, "$(nameof(mod)).") : Tuple{String, String}[]
enum_entries(other, prefix = "") = Tuple{String, String}[]
8 changes: 6 additions & 2 deletions src/abbreviations/module_names.jl
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,18 @@ const EXPORTS = ModuleExports()
function format(::ModuleExports, buf, doc)
object = Base.invokelatest(Docs.resolve, doc_binding(doc))::Module
# `names` also returns `public` names on Julia 1.11 and later.
return print_name_list(buf, object, filter(sym -> Base.invokelatest(Base.isexported, object, sym)::Bool, names(object)))
exported = Symbol[]
for sym in names(object)
Base.invokelatest(Base.isexported, object, sym)::Bool && push!(exported, sym)
end
return print_name_list(buf, object, exported)
end

function print_name_list(buf, object::Module, syms)
if !isempty(syms)
println(buf)
# Sorting ignores the `@` in macro names and sorts them in with others.
for sym in sort(syms, by = s -> lstrip(string(s), '@'))
for sym in insertion_sort!(copy(syms), (a, b) -> lstrip(string(a), '@') < lstrip(string(b), '@'))
# Skip the module itself, since that's always exported.
sym === nameof(object) && continue
# We print linked names using Documenter.jl cross-reference syntax
Expand Down
41 changes: 25 additions & 16 deletions src/abbreviations/signatures.jl
Original file line number Diff line number Diff line change
Expand Up @@ -76,21 +76,21 @@ function format(abbr::MethodSignatures, buf, doc)

return if !isempty(groups)
lines = String[]
io = IOBuffer()
for group in groups, method in collapse_defaults(group, defaults)
push!(
lines, sprint(
io -> printmethod(
io, binding, func, method;
definition = abbr.definition, returns = abbr.returns,
brackets = abbr.defaults === :brackets
)
)
printmethod(
io, binding, func, method;
definition = abbr.definition, returns = abbr.returns,
brackets = abbr.defaults === :brackets
)
push!(lines, String(take!(io)))
end
println(buf)
println(buf, "```julia")
# A struct's typed constructor and its untyped fallback print the same signature.
foreach(line -> println(buf, line), unique(lines))
for line in unique(lines)
println(buf, line)
end
println(buf, "```\n")
end
end
Expand Down Expand Up @@ -181,12 +181,18 @@ the method that takes every argument of `definition`.
"""
collapse_defaults(group, ::Nothing) = group
function collapse_defaults(group, definition::DefinitionArguments)
matching = filter(m -> matches_definition(m, definition), group)
isempty(matching) && return group
longest = maximum(m -> length(arguments(m)), matching)
return filter(group) do m
!matches_definition(m, definition) || length(arguments(m)) == longest
longest = nothing
for m in group
matches_definition(m, definition) || continue
n = length(arguments(m))
(longest === nothing || n > longest) && (longest = n)
end
longest === nothing && return group
kept = Method[]
for m in group
(!matches_definition(m, definition) || length(arguments(m)) == longest) && push!(kept, m)
end
return kept
end

function format(tms::TypedMethodSignatures, buf, doc)
Expand Down Expand Up @@ -227,9 +233,12 @@ function printed_typesig(func, method::Method, tuples)
if idx === nothing
n = length(arguments(method))
@static if Sys.iswindows() && VERSION < v"1.8"
idx = findlast(t -> has_arity(t, n), tuples)
candidates = length(tuples):-1:1
else
idx = findfirst(t -> has_arity(t, n), tuples)
candidates = 1:length(tuples)
end
for i in candidates
has_arity(tuples[i], n) && (idx = i; break)
end
end
# Julia 1.12 and later can normalise `typesig` to `Tuple`, leaving no tuple of length `n`.
Expand Down
3 changes: 2 additions & 1 deletion src/abbreviations/subtypes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ julia> @doc Shape
const SUBTYPES = TypeSubtypes()

function format(::TypeSubtypes, buf, doc)
names = Base.invokelatest(subtype_names ∘ Docs.resolve, doc_binding(doc))::Vector{String}
object = Base.invokelatest(Docs.resolve, doc_binding(doc))
names = Base.invokelatest(subtype_names, object)::Vector{String}
if !isempty(names)
println(buf)
for name in names
Expand Down
5 changes: 4 additions & 1 deletion src/abbreviations/typedef.jl
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ function print_params(buf, object, io)
isempty(object.parameters) && return
env = parameter_context(object, io)
print(buf, "{")
join(buf, map(p -> parameter_text(p, env), object.parameters), ", ")
for (i, p) in enumerate(object.parameters)
i > 1 && print(buf, ", ")
print(buf, parameter_text(p, env))
end
print(buf, "}")
return
end
Expand Down
Loading
Loading