Skip to content

Commit

Permalink
Replace isexported with ispublic to filter names in @autodocs (#…
Browse files Browse the repository at this point in the history
…2629)

Co-authored-by: Anshul Singhvi <[email protected]>
Co-authored-by: Morten Piibeleht <[email protected]>
  • Loading branch information
3 people authored Mar 5, 2025
1 parent d2bf676 commit 09eb5a5
Show file tree
Hide file tree
Showing 11 changed files with 92 additions and 16 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* Symlinks are now followed when walking the docs directory. ([#2610])
* PDF/LaTeX builds now throw a more informative error when `sitename` is not provided. ([#2636])
* `@autodocs` now lists public unexported symbols by default (i.e. when `Public = true`). ([#2629])

This is **potentially breaking** as it can cause previously working builds to fail if they are being run in strict mode.
Errors can happen if there are unexported symbols marked with `public` whose docstrings are being included manually with e.g. `@docs` blocks, and there is also an `@autodocs` block including docstrings for all public symbols.
The solution is to remove the duplicate inclusion.

* `checkdocs` has a new option `:public` to check that unexported symbols marked with `public` are included in the docs. ([#2629])

## Version [v1.8.1] - 2025-02-11

Expand Down Expand Up @@ -1940,6 +1947,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#2621]: https://github.com/JuliaDocs/Documenter.jl/issues/2621
[#2622]: https://github.com/JuliaDocs/Documenter.jl/issues/2622
[#2624]: https://github.com/JuliaDocs/Documenter.jl/issues/2624
[#2629]: https://github.com/JuliaDocs/Documenter.jl/issues/2629
[#2636]: https://github.com/JuliaDocs/Documenter.jl/issues/2636
[JuliaLang/julia#36953]: https://github.com/JuliaLang/julia/issues/36953
[JuliaLang/julia#38054]: https://github.com/JuliaLang/julia/issues/38054
Expand Down
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "Documenter"
uuid = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
version = "1.8.1"
version = "1.9.0"

[deps]
ANSIColoredPrinters = "a4c015fc-c6ff-483c-b24f-f7ea428134e9"
Expand Down
8 changes: 6 additions & 2 deletions docs/src/man/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,14 @@ Filter = myCustomFilterFunction
```
````

To include only the exported names from the modules listed in `Modules` use `Private = false`.
In a similar way `Public = false` can be used to only show the unexported names. By
To include only the public names from the modules listed in `Modules` use `Private = false`.
In a similar way `Public = false` can be used to only show the private names. By
default both of these are set to `true` so that all names will be shown.

!!! info
In Julia versions up to and including v1.10, "public" = "exported".
Starting with Julia v1.11, "public" = "exported _or_ marked with the `public` keyword".

````markdown
Functions exported from `Foo`:

Expand Down
39 changes: 39 additions & 0 deletions src/DocSystem.jl
Original file line number Diff line number Diff line change
Expand Up @@ -315,4 +315,43 @@ function parsedoc(docstr::DocStr)
return md
end

struct APIStatus
binding::Docs.Binding
isdefined::Bool
ispublic::Bool
isexported::Bool

function APIStatus(mod::Module, sym::Symbol)
isdefined = Base.isdefined(mod, sym)
ispublic = @static if isdefined(Base, :ispublic) # Julia v1.11 and later
Base.ispublic(mod, sym)
else
Base.isexported(mod, sym)
end
isexported = Base.isexported(mod, sym)
return new(Docs.Binding(mod, sym), isdefined, ispublic, isexported)
end
end

APIStatus(binding::Docs.Binding) = APIStatus(binding.mod, binding.var)

"""
This error message is reused in duplicate docstring warnings when we detect
the case when a duplicate docstring in a non-explored public name.
"""
function public_unexported_msg(apistatus::APIStatus)
return if apistatus.ispublic && !apistatus.isexported
"""\n
Note: this binding is marked `public`, but not exported. Starting from Documenter 1.9.0,
such bindings are now included in `@autodocs` blocks that list public APIs (blocks with `Public = true`),
but in older Documenter versions they would not have been. This may cause a previously
succeeding documentation build to fail because of duplicate docstrings, without any changes to your code. To fix this,
ensure that the same docstring is not included anywhere else (e.g. by an explicit `@docs`
block).
"""
else
""
end
end

end
9 changes: 8 additions & 1 deletion src/docchecks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,14 @@ function allbindings(checkdocs::Symbol, mod::Module, out = Dict{Binding, Set{Typ
# import/using.
name = nameof(binding)
isexported = (binding == Binding(mod, name)) && Base.isexported(mod, name)
if checkdocs === :all || (isexported && checkdocs === :exports)
ispublic = (binding == Binding(mod, name)) && @static if isdefined(Base, :ispublic)
Base.ispublic(mod, name)
else
Base.isexported(mod, name)
end
if checkdocs === :all ||
(isexported && checkdocs === :exports) ||
(ispublic && checkdocs === :public)
out[binding] = Set(sigs(doc))
end
end
Expand Down
2 changes: 1 addition & 1 deletion src/documents.jl
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ struct User
linkcheck_ignore::Vector{Union{String, Regex}} # ..and then ignore (some of) them.
linkcheck_timeout::Real # ..but only wait this many seconds for each one.
linkcheck_useragent::Union{String, Nothing} # User agent to use for linkchecks.
checkdocs::Symbol # Check objects missing from `@docs` blocks. `:none`, `:exports`, or `:all`.
checkdocs::Symbol # Check objects missing from `@docs` blocks. `:none`, `:exports`, `:public` or `:all`.
checkdocs_ignored_modules::Vector{Module} # ..and then ignore (some of) them.
doctestfilters::Vector{<:Any} # Filtering for doctests
warnonly::Vector{Symbol} # List of docerror groups that should only warn, rather than cause a build failure
Expand Down
15 changes: 8 additions & 7 deletions src/expander_pipeline.jl
Original file line number Diff line number Diff line change
Expand Up @@ -419,13 +419,14 @@ function Selectors.runner(::Type{Expanders.DocsBlocks}, node, page, doc)
object = make_object(binding, typesig, is_canonical, doc, page)
# We can't include the same object more than once in a document.
if haskey(doc.internal.objects, object)
apistatus = DocSystem.APIStatus(binding)
@docerror(
doc, :docs_block,
"""
duplicate docs found for '$(strip(str))' in `@docs` block in $(Documenter.locrepr(page.source, lines))
```$(x.info)
$(x.code)
```
``` $(DocSystem.public_unexported_msg(apistatus))
"""
)
push!(docsnodes, admonition)
Expand Down Expand Up @@ -524,8 +525,8 @@ function Selectors.runner(::Type{Expanders.AutoDocsBlocks}, node, page, doc)
for mod in modules
for (binding, multidoc) in Documenter.DocSystem.getmeta(mod)
# Which bindings should be included?
isexported = Base.isexported(mod, binding.var)
included = (isexported && public) || (!isexported && private)
apistatus = DocSystem.APIStatus(mod, binding.var)
included = (apistatus.ispublic && public) || (!apistatus.ispublic && private)
# What category does the binding belong to?
category = try
Documenter.DocSystem.category(binding)
Expand Down Expand Up @@ -564,11 +565,11 @@ function Selectors.runner(::Type{Expanders.AutoDocsBlocks}, node, page, doc)
path = normpath(docstr.data[:path])
object = make_object(binding, typesig, is_canonical, doc, page)
if isempty(pages)
push!(results, (mod, path, category, object, isexported, docstr))
push!(results, (mod, path, category, object, apistatus, docstr))
else
for p in pages
if endswith(path, p)
push!(results, (mod, p, category, object, isexported, docstr))
push!(results, (mod, p, category, object, apistatus, docstr))
break
end
end
Expand Down Expand Up @@ -603,15 +604,15 @@ function Selectors.runner(::Type{Expanders.AutoDocsBlocks}, node, page, doc)

# Finalise docstrings.
docsnodes = Node[]
for (mod, path, category, object, isexported, docstr) in results
for (mod, path, category, object, apistatus, docstr) in results
if haskey(doc.internal.objects, object)
@docerror(
doc, :autodocs_block,
"""
duplicate docs found for '$(object.binding)' in $(Documenter.locrepr(page.source, lines))
```$(x.info)
$(x.code)
```
``` $(DocSystem.public_unexported_msg(apistatus))
"""
)
continue
Expand Down
5 changes: 3 additions & 2 deletions src/makedocs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,9 @@ by setting `Draft = true` in an `@meta` block.
**`checkdocs`** instructs [`makedocs`](@ref) to check whether all names within the modules
defined in the `modules` keyword that have a docstring attached have the docstring also
listed in the manual (e.g. there's a `@docs` block with that docstring). Possible values
are `:all` (check all names; the default), `:exports` (check only exported names) and
`:none` (no checks are performed).
are `:all` (check all names; the default), `:exports` (check only exported names),
`:public` (check exported names and those marked with the `public` keyword in Julia ≥ 1.11),
and `:none` (no checks are performed).
By default, if the document check detect any errors, it will fail the documentation build.
This behavior can be relaxed with the `warnonly` or `checkdocs_ignored_modules` keywords.
Expand Down
13 changes: 12 additions & 1 deletion test/examples/pages/e.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
module E

export f_1, f_2, f_3
# https://discourse.julialang.org/t/is-compat-jl-worth-it-for-the-public-keyword/119041/
macro public_or_export(ex)
args = ex isa Symbol ? (ex,) : Base.isexpr(ex, :tuple) ? ex.args : error()
return if Base.isdefined(Base, :ispublic)
esc(Expr(:public, args...))
else
esc(Expr(:export, args...))
end
end

export f_1, f_2
@public_or_export f_3

"f_1"
f_1(x) = x
Expand Down
2 changes: 1 addition & 1 deletion test/missingdocs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ module MissingDocsSubmodule
end

@testset "missing docs" begin
for (sym, n_expected) in zip([:none, :exports, :all], [0, 1, 2])
for (sym, n_expected) in zip([:none, :exports, :public, :all], [0, 1, 1, 2])
kwargs = (
root = dirname(@__FILE__),
source = joinpath("src", string(sym)),
Expand Down
5 changes: 5 additions & 0 deletions test/missingdocs/src/public/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# MissingDocs Exports

```@docs
Main.MissingDocs.f
```

0 comments on commit 09eb5a5

Please sign in to comment.