diff --git a/Project.toml b/Project.toml index 3647369..fd305d1 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "SciMLTesting" uuid = "09d9d899-5365-40a9-917a-5f67fddea283" authors = ["SciML"] -version = "2.6.2" +version = "2.6.3" [deps] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" diff --git a/src/SciMLTesting.jl b/src/SciMLTesting.jl index 0199f8e..bded9f4 100644 --- a/src/SciMLTesting.jl +++ b/src/SciMLTesting.jl @@ -1031,6 +1031,23 @@ else end end +# A module name that is public only because the module itself was reexported. Julia +# exports a module's own name, so `using Dep` inside a facade puts `:Dep` into the +# facade's public API; the docstring obligation for it belongs to the package that +# defines the module, not to every reexporter. Deliberately restricted to `Module` +# values: a reexported function, type or constant still owes a docstring, since that +# one resolves through the binding to the defining package's docs. +function _is_external_module_reexport(pkg::Module, name::Symbol) + isdefined(pkg, name) || return false + value = try + getfield(pkg, name) + catch + return false + end + value isa Module || return false + return !_is_within_module(value, pkg) +end + # Only names owned by this package's module hierarchy require a local rendered API # entry. External public reexports are audited separately by `public_reexports`. function _requires_local_rendering(pkg::Module, name::Symbol) @@ -1141,7 +1158,10 @@ Two checks, each its own nested `@testset`: * **docstrings** (`docstrings = true`, on by default): every public API name has a docstring. A re-exported name documented in its defining package counts as documented (the check follows the binding, not a local docstring), so a repo is - not forced to redocument names it re-exports from a dependency. + not forced to redocument names it re-exports from a dependency. A re-exported + *module* name — public only because `using Dep` exports `Dep` itself — is exempt + entirely, mirroring the rendered check; the obligation to document `Dep` belongs + to `Dep`. Re-exported functions, types and constants are not exempt. * **rendered** (`rendered = true`, on by default): every public API name appears inside a ```` ```@docs ```` block somewhere under `docs_src`, so it is rendered in the manual. Re-exported modules inherit the defining package's rendered module @@ -1199,7 +1219,13 @@ function run_api_docs( @testset "$testset" begin if docstrings skip = Set{Symbol}(Symbol.(ignore)) - undocumented = sort!(filter(n -> !(n in skip) && !_has_docstring(pkg, n), api)) + undocumented = sort!( + filter( + n -> !(n in skip) && !_is_external_module_reexport(pkg, n) && + !_has_docstring(pkg, n), + api, + ) + ) @testset "public API has docstrings" begin docstrings_broken ? (@test_broken isempty(undocumented)) : (@test isempty(undocumented)) diff --git a/test/runtests.jl b/test/runtests.jl index 917d2c2..99089b7 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -201,6 +201,15 @@ module LocalModuleFixture export LocalSubmodule end +# Reexports an undocumented external module and an undocumented external function +# next to an undocumented local name, so the docstrings check can be shown to exempt +# the module and only the module. +module UndocumentedModuleReexportFixture + import ..ReexportOwnerFixture: OwnedModule, owned_function + export OwnedModule, owned_function, local_undocumented + local_undocumented() = nothing +end + # A minimal AbstractTestSet that just collects every recorded result (including # nested testsets) and NEVER throws on finish. Wrapping a run_qa call in one lets a # test inspect the Broken/Pass/Fail/Error counts a broken-marker produced without @@ -1145,6 +1154,48 @@ end @test c[:fail] == 0 && c[:error] == 0 @test c[:pass] == 1 + # A reexported external module is public only because Julia exports a module's + # own name; documenting it belongs to the package that defines it. + c = counts_of() do + run_api_docs( + UndocumentedModuleReexportFixture; + rendered = false, + ignore = (:owned_function, :local_undocumented), + ) + end + @test c[:fail] == 0 && c[:error] == 0 + + # The exemption covers modules only: a reexported function is still reported. + c = counts_of() do + run_api_docs( + UndocumentedModuleReexportFixture; + rendered = false, + ignore = (:local_undocumented,), + ) + end + @test c[:fail] == 1 + + @test SciMLTesting._is_external_module_reexport( + UndocumentedModuleReexportFixture, :OwnedModule + ) + @test !SciMLTesting._is_external_module_reexport( + UndocumentedModuleReexportFixture, :owned_function + ) + @test !SciMLTesting._is_external_module_reexport( + LocalModuleFixture, :LocalSubmodule + ) + + # A package's own undocumented submodule is still the package's to document. + # Only on 1.11+: the 1.10 `@doc` fallback renders an undocumented module as + # "No docstring or readme file found", which `_has_docstring`'s substring never + # matched, so no module is ever reported as undocumented there. + @static if VERSION >= v"1.11" + c = counts_of() do + run_api_docs(LocalModuleFixture; rendered = false) + end + @test c[:fail] == 1 + end + # docstrings_broken records Broken while names remain undocumented (migration). c = counts_of() do run_api_docs(ApiFixture; rendered = false, docstrings_broken = true)