diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a6cdabb81..23ff5dd8e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +* ![Bugfix][badge-bugfix] Fix `strict` mode to properly print errors, not just a warnings. ([#1756][github-1756], [#1776][github-1776]) ## Version `v0.27.15` @@ -980,6 +981,7 @@ [github-1751]: https://github.com/JuliaDocs/Documenter.jl/pull/1751 [github-1752]: https://github.com/JuliaDocs/Documenter.jl/pull/1752 [github-1754]: https://github.com/JuliaDocs/Documenter.jl/pull/1754 +[github-1756]: https://github.com/JuliaDocs/Documenter.jl/issues/1756 [github-1758]: https://github.com/JuliaDocs/Documenter.jl/issues/1758 [github-1759]: https://github.com/JuliaDocs/Documenter.jl/pull/1759 [github-1760]: https://github.com/JuliaDocs/Documenter.jl/issues/1760 @@ -989,6 +991,7 @@ [github-1772]: https://github.com/JuliaDocs/Documenter.jl/issues/1772 [github-1773]: https://github.com/JuliaDocs/Documenter.jl/pull/1773 [github-1774]: https://github.com/JuliaDocs/Documenter.jl/pull/1774 +[github-1776]: https://github.com/JuliaDocs/Documenter.jl/pull/1776 [julia-38079]: https://github.com/JuliaLang/julia/issues/38079 [julia-39841]: https://github.com/JuliaLang/julia/pull/39841 diff --git a/src/CrossReferences.jl b/src/CrossReferences.jl index 3776b59b9a..3c2ad4efe6 100644 --- a/src/CrossReferences.jl +++ b/src/CrossReferences.jl @@ -9,7 +9,8 @@ import ..Documenter: Documents, Expanders, Documenter, - Utilities + Utilities, + Utilities.@docerror using DocStringExtensions import Markdown @@ -70,8 +71,7 @@ function namedxref(link::Markdown.Link, meta, page, doc) slug = match(NAMED_XREF, link.url)[1] if isempty(slug) text = sprint(Markdown.plaininline, link) - push!(doc.internal.errors, :cross_references) - @warn "'$text' missing a name after '#' in $(Utilities.locrepr(page.source))." + @docerror(doc, :cross_references, "'$text' missing a name after '#' in $(Utilities.locrepr(page.source)).") else if Anchors.exists(doc.internal.headers, slug) namedxref(link, slug, meta, page, doc) @@ -96,12 +96,10 @@ function namedxref(link::Markdown.Link, slug, meta, page, doc) path = relpath(anchor.file, dirname(page.build)) link.url = string(path, Anchors.fragment(anchor)) else - push!(doc.internal.errors, :cross_references) - @warn "'$slug' is not unique in $(Utilities.locrepr(page.source))." + @docerror(doc, :cross_references, "'$slug' is not unique in $(Utilities.locrepr(page.source)).") end else - push!(doc.internal.errors, :cross_references) - @warn "reference for '$slug' could not be found in $(Utilities.locrepr(page.source))." + @docerror(doc, :cross_references, "reference for '$slug' could not be found in $(Utilities.locrepr(page.source)).") end end @@ -121,8 +119,7 @@ function docsxref(link::Markdown.Link, code, meta, page, doc) ex = Meta.parse(code) catch err !isa(err, Meta.ParseError) && rethrow(err) - push!(doc.internal.errors, :cross_references) - @warn "unable to parse the reference '[`$code`](@ref)' in $(Utilities.locrepr(page.source))." + @docerror(doc, :cross_references, "unable to parse the reference '[`$code`](@ref)' in $(Utilities.locrepr(page.source)).") return end end @@ -133,8 +130,7 @@ function docsxref(link::Markdown.Link, code, meta, page, doc) try binding = Documenter.DocSystem.binding(mod, ex) catch err - push!(doc.internal.errors, :cross_references) - @warn "unable to get the binding for '[`$code`](@ref)' in $(Utilities.locrepr(page.source)) from expression '$(repr(ex))' in module $(mod)" exception = err + @docerror(doc, :cross_references, "unable to get the binding for '[`$code`](@ref)' in $(Utilities.locrepr(page.source)) from expression '$(repr(ex))' in module $(mod)", exception = err) return end @@ -142,8 +138,7 @@ function docsxref(link::Markdown.Link, code, meta, page, doc) try typesig = Core.eval(mod, Documenter.DocSystem.signature(ex, rstrip(code))) catch err - push!(doc.internal.errors, :cross_references) - @warn "unable to evaluate the type signature for '[`$code`](@ref)' in $(Utilities.locrepr(page.source)) from expression '$(repr(ex))' in module $(mod)" exception = err + @docerror(doc, :cross_references, "unable to evaluate the type signature for '[`$code`](@ref)' in $(Utilities.locrepr(page.source)) from expression '$(repr(ex))' in module $(mod)", exception = err) return end @@ -156,8 +151,7 @@ function docsxref(link::Markdown.Link, code, meta, page, doc) slug = Utilities.slugify(object) link.url = string(path, '#', slug) else - push!(doc.internal.errors, :cross_references) - @warn "no doc found for reference '[`$code`](@ref)' in $(Utilities.locrepr(page.source))." + @docerror(doc, :cross_references, "no doc found for reference '[`$code`](@ref)' in $(Utilities.locrepr(page.source)).") end end diff --git a/src/DocChecks.jl b/src/DocChecks.jl index 028e03bc77..191719c2d7 100644 --- a/src/DocChecks.jl +++ b/src/DocChecks.jl @@ -9,14 +9,11 @@ import ..Documenter: Documents, Utilities, Utilities.Markdown2, - Builder.is_strict + Utilities.@docerror using DocStringExtensions import Markdown -using Logging -loglevel(doc, val) = is_strict(doc.user.strict, val) ? Logging.Error : Logging.Warn - # Missing docstrings. # ------------------- @@ -66,8 +63,7 @@ function missingdocs(doc::Documents.Document) println(b, """\n These are docstrings in the checked modules (configured with the modules keyword) that are not included in @docs or @autodocs blocks.""") - push!(doc.internal.errors, :missing_docs) - @logmsg loglevel(doc, :missing_docs) String(take!(b)) + @docerror(doc, :missing_docs, String(take!(b))) end end @@ -129,18 +125,15 @@ function footnotes(doc::Documents.Document) for (id, (ids, bodies)) in orphans # Multiple footnote bodies. if bodies > 1 - push!(doc.internal.errors, :footnote) - @logmsg loglevel(doc, :footnote) "footnote '$id' has $bodies bodies in $(Utilities.locrepr(page.source))." + @docerror(doc, :footnote, "footnote '$id' has $bodies bodies in $(Utilities.locrepr(page.source)).") end # No footnote references for an id. if ids === 0 - push!(doc.internal.errors, :footnote) - @logmsg loglevel(doc, :footnote) "unused footnote named '$id' in $(Utilities.locrepr(page.source))." + @docerror(doc, :footnote, "unused footnote named '$id' in $(Utilities.locrepr(page.source)).") end # No footnote bodies for an id. if bodies === 0 - push!(doc.internal.errors, :footnote) - @logmsg loglevel(doc, :footnote) "no footnotes found for '$id' in $(Utilities.locrepr(page.source))." + @docerror(doc, :footnote, "no footnotes found for '$id' in $(Utilities.locrepr(page.source)).") end end end @@ -182,8 +175,7 @@ function linkcheck(doc::Documents.Document) end end else - push!(doc.internal.errors, :linkcheck) - @logmsg loglevel(doc, :linkcheck) "linkcheck requires `curl`." + @docerror(doc, :linkcheck, "linkcheck requires `curl`.") end end return nothing @@ -209,8 +201,7 @@ function linkcheck(link::Markdown.Link, doc::Documents.Document; method::Symbol= # interpolating into backticks escapes spaces so constructing a Cmd is necessary result = read(cmd, String) catch err - push!(doc.internal.errors, :linkcheck) - @logmsg loglevel(doc, :linkcheck) "$cmd failed:" exception = err + @docerror(doc, :linkcheck, "$cmd failed:", exception = err) return false end STATUS_REGEX = r"^(\d+) (\w+)://(?:\S+) (\S+)?$"m @@ -240,12 +231,10 @@ function linkcheck(link::Markdown.Link, doc::Documents.Document; method::Symbol= @debug "linkcheck '$(link.url)' status: $(status), retrying without `-I`" return linkcheck(link, doc; method=:GET) else - push!(doc.internal.errors, :linkcheck) - @logmsg loglevel(doc, :linkcheck) "linkcheck '$(link.url)' status: $(status)." + @docerror(doc, :linkcheck, "linkcheck '$(link.url)' status: $(status).") end else - push!(doc.internal.errors, :linkcheck) - @logmsg loglevel(doc, :linkcheck) "invalid result returned by $cmd:" result + @docerror(doc, :linkcheck, "invalid result returned by $cmd:", result) end end return false diff --git a/src/DocTests.jl b/src/DocTests.jl index e3a33364e5..c24ac384a2 100644 --- a/src/DocTests.jl +++ b/src/DocTests.jl @@ -15,7 +15,7 @@ import ..Documenter: IdDict import Markdown, REPL -import .Utilities: Markdown2 +import .Utilities: Markdown2, @docerror import IOCapture # Julia code block testing. @@ -97,8 +97,7 @@ function parse_metablock(ctx::DocTestContext, block::Markdown2.CodeBlock) try meta[ex.args[1]] = Core.eval(Main, ex.args[2]) catch err - push!(ctx.doc.internal.errors, :meta_block) - @warn "Failed to evaluate `$(strip(str))` in `@meta` block." err + @docerror(ctx.doc, :meta_block, "Failed to evaluate `$(strip(str))` in `@meta` block.", exception = err) end end end @@ -138,10 +137,10 @@ function doctest(ctx::DocTestContext, block_immutable::Markdown2.CodeBlock) Meta.parse("($(lang[nextind(lang, idx):end]),)") catch e e isa Meta.ParseError || rethrow(e) - push!(ctx.doc.internal.errors, :doctest) file = ctx.meta[:CurrentFile] lines = Utilities.find_block_in_file(block.code, file) - @warn(""" + @docerror(ctx.doc, :doctest, + """ Unable to parse doctest keyword arguments in $(Utilities.locrepr(file, lines)) Use ```jldoctest name; key1 = value1, key2 = value2 @@ -153,10 +152,10 @@ function doctest(ctx::DocTestContext, block_immutable::Markdown2.CodeBlock) end for kwarg in kwargs.args if !(isa(kwarg, Expr) && kwarg.head === :(=) && isa(kwarg.args[1], Symbol)) - push!(ctx.doc.internal.errors, :doctest) file = ctx.meta[:CurrentFile] lines = Utilities.find_block_in_file(block.code, file) - @warn(""" + @docerror(ctx.doc, :doctest, + """ invalid syntax for doctest keyword arguments in $(Utilities.locrepr(file, lines)) Use ```jldoctest name; key1 = value1, key2 = value2 @@ -187,10 +186,10 @@ function doctest(ctx::DocTestContext, block_immutable::Markdown2.CodeBlock) elseif occursin(r"^# output$"m, block.code) eval_script(block, sandbox, ctx.meta, ctx.doc, ctx.file) else - push!(ctx.doc.internal.errors, :doctest) file = ctx.meta[:CurrentFile] lines = Utilities.find_block_in_file(block.code, file) - @warn(""" + @docerror(ctx.doc, :doctest, + """ invalid doctest block in $(Utilities.locrepr(file, lines)) Requires `julia> ` or `# output` diff --git a/src/Expanders.jl b/src/Expanders.jl index c6d73de635..488819a674 100644 --- a/src/Expanders.jl +++ b/src/Expanders.jl @@ -16,7 +16,7 @@ import .Documents: EvalNode, MetaNode -import .Utilities: Selectors +import .Utilities: Selectors, @docerror import Markdown, REPL import Base64: stringmime @@ -256,8 +256,8 @@ function Selectors.runner(::Type{MetaBlocks}, x, page, doc) try meta[ex.args[1]] = Core.eval(Main, ex.args[2]) catch err - push!(doc.internal.errors, :meta_block) - @warn(""" + @docerror(doc, :meta_block, + """ failed to evaluate `$(strip(str))` in `@meta` block in $(Utilities.locrepr(page.source, lines)) ```$(x.language) $(x.code) @@ -283,8 +283,8 @@ function Selectors.runner(::Type{DocsBlocks}, x, page, doc) binding = try Documenter.DocSystem.binding(curmod, ex) catch err - push!(doc.internal.errors, :docs_block) - @warn(""" + @docerror(doc, :docs_block, + """ unable to get the binding for '$(strip(str))' in `@docs` block in $(Utilities.locrepr(page.source, lines)) from expression '$(repr(ex))' in module $(curmod) ```$(x.language) $(x.code) @@ -296,8 +296,8 @@ function Selectors.runner(::Type{DocsBlocks}, x, page, doc) end # Undefined `Bindings` get discarded. if !Documenter.DocSystem.iskeyword(binding) && !Documenter.DocSystem.defined(binding) - push!(doc.internal.errors, :docs_block) - @warn(""" + @docerror(doc, :docs_block, + """ undefined binding '$(binding)' in `@docs` block in $(Utilities.locrepr(page.source, lines)) ```$(x.language) $(x.code) @@ -311,8 +311,8 @@ function Selectors.runner(::Type{DocsBlocks}, x, page, doc) object = Utilities.Object(binding, typesig) # We can't include the same object more than once in a document. if haskey(doc.internal.objects, object) - push!(doc.internal.errors, :docs_block) - @warn(""" + @docerror(doc, :docs_block, + """ duplicate docs found for '$(strip(str))' in `@docs` block in $(Utilities.locrepr(page.source, lines)) ```$(x.language) $(x.code) @@ -332,8 +332,8 @@ function Selectors.runner(::Type{DocsBlocks}, x, page, doc) # Check that we aren't printing an empty docs list. Skip block when empty. if isempty(docs) - push!(doc.internal.errors, :docs_block) - @warn(""" + @docerror(doc, :docs_block, + """ no docs found for '$(strip(str))' in `@docs` block in $(Utilities.locrepr(page.source, lines)) ```$(x.language) $(x.code) @@ -383,8 +383,8 @@ function Selectors.runner(::Type{AutoDocsBlocks}, x, page, doc) fields[ex.args[1]] = Core.eval(curmod, ex.args[2]) end catch err - push!(doc.internal.errors, :autodocs_block) - @warn(""" + @docerror(doc, :autodocs_block, + """ failed to evaluate `$(strip(str))` in `@autodocs` block in $(Utilities.locrepr(page.source, lines)) ```$(x.language) $(x.code) @@ -451,8 +451,8 @@ function Selectors.runner(::Type{AutoDocsBlocks}, x, page, doc) nodes = DocsNode[] for (mod, path, category, object, isexported, docstr) in results if haskey(doc.internal.objects, object) - push!(doc.internal.errors, :autodocs_block) - @warn(""" + @docerror(doc, :autodocs_block, + """ duplicate docs found for '$(object.binding)' in $(Utilities.locrepr(page.source, lines)) ```$(x.language) $(x.code) @@ -475,8 +475,8 @@ function Selectors.runner(::Type{AutoDocsBlocks}, x, page, doc) end page.mapping[x] = DocsNodes(nodes) else - push!(doc.internal.errors, :autodocs_block) - @warn(""" + @docerror(doc, :autodocs_block, + """ '@autodocs' missing 'Modules = ...' in $(Utilities.locrepr(page.source, lines)) ```$(x.language) $(x.code) @@ -502,8 +502,8 @@ function Selectors.runner(::Type{EvalBlocks}, x, page, doc) try result = Core.eval(sandbox, ex) catch err - push!(doc.internal.errors, :eval_block) - @warn(""" + @docerror(doc, :eval_block, + """ failed to evaluate `@eval` block in $(Utilities.locrepr(page.source)) ```$(x.language) $(x.code) @@ -587,13 +587,13 @@ function Selectors.runner(::Type{ExampleBlocks}, x, page, doc) result = c.value print(buffer, c.output) if c.error - push!(doc.internal.errors, :example_block) - @warn(""" + @docerror(doc, :example_block, + """ failed to run `@example` block in $(Utilities.locrepr(page.source, lines)) ```$(x.language) $(x.code) ``` - """, c.value) + """, value = c.value) page.mapping[x] = x return end @@ -714,8 +714,8 @@ function Selectors.runner(::Type{SetupBlocks}, x, page, doc) end Markdown.MD([]) catch err - push!(doc.internal.errors, :setup_block) - @warn(""" + @docerror(doc, :setup_block, + """ failed to run `@setup` block in $(Utilities.locrepr(page.source)) ```$(x.language) $(x.code) diff --git a/src/Utilities/Utilities.jl b/src/Utilities/Utilities.jl index 05d888e38e..c4cdbf7346 100644 --- a/src/Utilities/Utilities.jl +++ b/src/Utilities/Utilities.jl @@ -10,6 +10,33 @@ import Markdown, LibGit2 import Base64: stringmime import ..ERROR_NAMES +""" + @docerror(doc, tag, msg, exs...) + +Add `tag` to the `doc.internal.errors` array and log the message `msg` as an +error (if `tag` matches the `doc.user.strict` setting) or warning. + +- `doc` must be the instance of `Document` used for the Documenter run +- `tag` must be one of the `Symbol`s in `ERROR_NAMES` +- `msg` is the explanation of the issue to the user +- `exs...` are additional expressions that will be included with the message; + see `@error` and `@warn` +""" +macro docerror(doc, tag, msg, exs...) + tag isa QuoteNode || error("invalid call of @docerror") + tag.value ∈ ERROR_NAMES || throw(ArgumentError("tag $(tag) is not a valid Documenter error")) + esc(quote + let + push!($(doc).internal.errors, $(tag)) + if $Utilities.is_strict($(doc).user.strict, $(tag)) + @error $(msg) $(exs...) + else + @warn $(msg) $(exs...) + end + end + end) +end + # escape characters that has a meaning in regex regex_escape(str) = sprint(escape_string, str, "\\^\$.|?*+()[{") @@ -113,8 +140,7 @@ function parseblock(code::AbstractString, doc, file; skip = 0, keywords = true, try Meta.parse(code, cursor; raise=raise) catch err - push!(doc.internal.errors, :parse_error) - @warn "failed to parse exception in $(Utilities.locrepr(file))" exception = err + @docerror(doc, :parse_error, "failed to parse exception in $(Utilities.locrepr(file))", exception = err) break end end diff --git a/test/utilities.jl b/test/utilities.jl index 4d1c0c899b..4430d79f6d 100644 --- a/test/utilities.jl +++ b/test/utilities.jl @@ -522,6 +522,23 @@ end @test_throws ArgumentError Documenter.Utilities.check_strict_kw(:a) @test_throws ArgumentError Documenter.Utilities.check_strict_kw([:a, :doctest]) end + + @testset "@docerror" begin + doc = (; internal = (; errors = Symbol[]), user = (; strict = [:doctest, :setup_block])) + foo = 123 + @test_logs (:warn, "meta_block issue 123") (Documenter.Utilities.@docerror(doc, :meta_block, "meta_block issue $foo")) + @test :meta_block ∈ doc.internal.errors + @test_logs (:error, "doctest issue 123") (Documenter.Utilities.@docerror(doc, :doctest, "doctest issue $foo")) + @test :doctest ∈ doc.internal.errors + try + @macroexpand Documenter.Utilities.@docerror(doc, :foo, "invalid tag") + error("unexpected") + catch err + err isa LoadError && (err = err.error) + @test err isa ArgumentError + @test err.msg == "tag :foo is not a valid Documenter error" + end + end end end