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
7 changes: 5 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
name = "ExplicitImports"
uuid = "7d51a73a-1435-4ff3-83d9-f097790105c7"
version = "1.14.2"
version = "1.15.0"
authors = ["Eric P. Hanson"]

[deps]
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[compat]
Aqua = "0.8.4"
Expand All @@ -16,6 +17,7 @@ DataFrames = "1.6"
LinearAlgebra = "<0.0.1, 1"
Logging = "<0.0.1, 1"
Markdown = "<0.0.1, 1"
MetaTesting = "0.1"
Pkg = "<0.0.1, 1"
PrecompileTools = "1.2"
Reexport = "1.2.2"
Expand All @@ -32,9 +34,10 @@ Compat = "34da2185-b29b-5c13-b0c7-acf172513d20"
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
MetaTesting = "9e32d19f-1e4f-477a-8631-b16c78aa0f56"
Reexport = "189a3867-3050-52da-a836-e630ba90ab69"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"

[targets]
test = ["Aqua", "Compat", "DataFrames", "LinearAlgebra", "Logging", "UUIDs", "Reexport", "Test"]
test = ["Aqua", "Compat", "DataFrames", "LinearAlgebra", "Logging", "UUIDs", "Reexport", "MetaTesting", "Test"]
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ To understand these examples, note that:
- `map` is an API function of Base, which happens to be present in the LinearAlgebra namespace
- `_svd!` is a private function of LinearAlgebra

ExplicitImports also supplies Test.jl-style wrappers around each individual check, named with `test_*` instead of `check_*` (e.g. `test_no_implicit_imports`). The `test_*` functions can be all invoked together with the helper function `test_explicit_imports`. These differ from the `check_*` functions in two ways:

- they use a testset and result in test failures instead of test errors
- they always print the locations in the failure message for easier debugging

## Goals

- Figure out what implicit imports a Julia module is relying on, in order to make them explicit.
Expand Down
22 changes: 21 additions & 1 deletion docs/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,15 @@ improper_qualified_accesses

ExplicitImports.jl provides several functions (all starting with `check_`) which introspect a module for various kinds of potential issues, and throws errors if these issues are encountered. These "check" functions are designed to be narrowly scoped to detect one specific type of issue, and stable so that they can be used in testing environments (with the aim that non-breaking releases of ExplicitExports.jl will generally not cause new test failures).

The first such check is [`check_no_implicit_imports`](@ref) which aims to ensure there are no implicit exports used in the package.
ExplicitImports also provides Test.jl wrappers for each `check_*` function. These are named `test_*` (e.g. [`test_no_implicit_imports`](@ref)) and mirror the keyword arguments of their corresponding checks.

Additionally, one can use [`test_explicit_imports`](@ref) to run _all_ of the tests from one simple interface. This functionality is modeled after [Aqua](https://github.com/JuliaTesting/Aqua.jl)'s [`Aqua.test_all`](https://juliatesting.github.io/Aqua.jl/stable/test_all/#Aqua.test_all).

```@docs
test_explicit_imports
```

Now let us go over each check in more detail. The first such check is [`check_no_implicit_imports`](@ref) which aims to ensure there are no implicit exports used in the package.

```@docs
check_no_implicit_imports
Expand All @@ -47,6 +55,18 @@ check_all_qualified_accesses_are_public
check_no_self_qualified_accesses
```

There are also Test.jl versions of these which use testsets instead of throwing errors:

```@docs
test_no_implicit_imports
test_no_stale_explicit_imports
test_all_explicit_imports_via_owners
test_all_explicit_imports_are_public
test_all_qualified_accesses_via_owners
test_all_qualified_accesses_are_public
test_no_self_qualified_accesses
```

## Usage with scripts (such as `runtests.jl`)

We also provide a helper function to analyze scripts (rather than modules).
Expand Down
7 changes: 7 additions & 0 deletions src/ExplicitImports.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ using TOML: TOML, parsefile
using Markdown: Markdown
using PrecompileTools: @setup_workload, @compile_workload
using Pkg: Pkg
using Test: Test, @test, @testset

# we'll borrow their `@_public` macro; if this goes away, we can get our own
JuliaSyntax.@_public ignore_submodules
Expand All @@ -41,6 +42,11 @@ export ImplicitImportsException, UnanalyzableModuleException,
NonPublicQualifiedAccessException, SelfQualifiedAccessException
export StaleImportsException, check_no_stale_explicit_imports

export test_explicit_imports, test_no_implicit_imports, test_no_stale_explicit_imports,
test_all_explicit_imports_via_owners, test_all_explicit_imports_are_public,
test_all_qualified_accesses_via_owners, test_all_qualified_accesses_are_public,
test_no_self_qualified_accesses

# deprecated
export print_stale_explicit_imports, stale_explicit_imports,
stale_explicit_imports_nonrecursive,
Expand All @@ -67,6 +73,7 @@ include("improper_qualified_accesses.jl")
include("improper_explicit_imports.jl")
include("interactive_usage.jl")
include("checks.jl")
include("test_explicit_imports.jl")
include("deprecated.jl")
include("main.jl")

Expand Down
110 changes: 79 additions & 31 deletions src/checks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ end
Checks that neither `mod` nor any of its submodules has stale (unused) explicit imports, throwing
an `StaleImportsException` if so, and returning `nothing` otherwise.

If `throw=false`, return the exception instead of throwing.

This can be used in a package's tests, e.g.

```julia
Expand All @@ -151,20 +153,25 @@ that are allowed to be stale explicit imports. For example,
would check there were no stale explicit imports besides that of the name `DataFrame`.
"""
function check_no_stale_explicit_imports(mod::Module, file=pathof(mod); ignore::Tuple=(),
allow_unanalyzable::Tuple=())
allow_unanalyzable::Tuple=(), throw=true,
# private undocumented kwarg for hoisting this analysis
file_analysis=Dict())
check_file(file)
for (submodule, stale_imports) in
improper_explicit_imports(mod, file; strict=true, allow_internal_imports=false)
improper_explicit_imports(mod, file; strict=true, allow_internal_imports=false,
file_analysis)
if isnothing(stale_imports)
submodule in allow_unanalyzable && continue
throw(UnanalyzableModuleException(submodule))
ex = UnanalyzableModuleException(submodule)
throw ? Base.throw(ex) : return ex
end
filter!(stale_imports) do nt
return nt.name ∉ ignore && nt.stale
end
if !isempty(stale_imports)
throw(StaleImportsException(submodule,
NamedTuple{(:name, :location)}.(stale_imports)))
ex = StaleImportsException(submodule,
NamedTuple{(:name, :location)}.(stale_imports))
throw ? Base.throw(ex) : return ex
end
end
return nothing
Expand All @@ -177,6 +184,8 @@ end
Checks that neither `mod` nor any of its submodules is relying on implicit imports, throwing
an `ImplicitImportsException` if so, and returning `nothing` otherwise.

If `throw=false`, return the exception instead of throwing.

This function can be used in a package's tests, e.g.

```julia
Expand Down Expand Up @@ -221,16 +230,24 @@ This would:
but verify there are no other implicit imports.
"""
function check_no_implicit_imports(mod::Module, file=pathof(mod); skip=(mod, Base, Core),
ignore::Tuple=(), allow_unanalyzable::Tuple=())
ignore::Tuple=(), allow_unanalyzable::Tuple=(),
throw=true,
# private undocumented kwarg for hoisting this analysis
file_analysis=Dict())
check_file(file)
ee = explicit_imports(mod, file; skip)
ee = explicit_imports(mod, file; skip, file_analysis)
for (submodule, names) in ee
if isnothing(names) && submodule in allow_unanalyzable
continue
if isnothing(names)
if submodule in allow_unanalyzable || should_ignore_module(submodule; ignore)
continue
end
ex = UnanalyzableModuleException(submodule)
throw ? Base.throw(ex) : return ex
end
should_ignore!(names, submodule; ignore)
if !isnothing(names) && !isempty(names)
throw(ImplicitImportsException(submodule, names))
if !isempty(names)
ex = ImplicitImportsException(submodule, names)
throw ? Base.throw(ex) : return ex
end
end
return nothing
Expand All @@ -249,15 +266,13 @@ function should_ignore!(names, mod; ignore)
end
end

function should_ignore!(::Nothing, mod; ignore)
function should_ignore_module(mod; ignore)
for elt in ignore
# we're ignoring this whole module
if elt == mod
return
return true
end
end
# Not ignored, and unanalyzable
throw(UnanalyzableModuleException(mod))
return false
end

"""
Expand All @@ -271,6 +286,8 @@ end
Checks that neither `mod` nor any of its submodules has accesses to names via modules other than their owner as determined by `Base.which` (unless the name is public or exported in that module),
throwing an `QualifiedAccessesFromNonOwnerException` if so, and returning `nothing` otherwise.

If `throw=false`, return the exception instead of throwing.

This can be used in a package's tests, e.g.

```julia
Expand Down Expand Up @@ -307,10 +324,14 @@ function check_all_qualified_accesses_via_owners(mod::Module, file=pathof(mod);
ignore::Tuple=(),
skip::TUPLE_MODULE_PAIRS=get_default_skip_pairs(),
require_submodule_access=false,
allow_internal_accesses=true)
allow_internal_accesses=true,
throw=true,
# private undocumented kwarg for hoisting this analysis
file_analysis=Dict())
check_file(file)
for (submodule, problematic) in
improper_qualified_accesses(mod, file; skip, allow_internal_accesses)
improper_qualified_accesses(mod, file; skip, allow_internal_accesses,
file_analysis)
filter!(problematic) do nt
return nt.name ∉ ignore
end
Expand All @@ -331,7 +352,8 @@ function check_all_qualified_accesses_via_owners(mod::Module, file=pathof(mod);
# drop unnecessary columns
problematic = NamedTuple{(:name, :location, :value, :accessing_from, :whichmodule)}.(problematic)
if !isempty(problematic)
throw(QualifiedAccessesFromNonOwnerException(submodule, problematic))
ex = QualifiedAccessesFromNonOwnerException(submodule, problematic)
throw ? Base.throw(ex) : return ex
end
end
return nothing
Expand All @@ -345,6 +367,8 @@ end
Checks that neither `mod` nor any of its submodules has qualified accesses to names which are non-public (i.e. not exported, nor declared public on Julia 1.11+)
throwing an `NonPublicQualifiedAccessException` if so, and returning `nothing` otherwise.

If `throw=false`, return the exception instead of throwing.

This can be used in a package's tests, e.g.

```julia
Expand Down Expand Up @@ -391,11 +415,15 @@ function check_all_qualified_accesses_are_public(mod::Module, file=pathof(mod);
skip::TUPLE_MODULE_PAIRS=(Base => Core,),
from=nothing,
ignore::Tuple=(),
allow_internal_accesses=true)
allow_internal_accesses=true,
throw=true,
# private undocumented kwarg for hoisting this analysis
file_analysis=Dict())
check_file(file)
for (submodule, problematic) in
# We pass `skip=()` since we will do our own filtering after
improper_qualified_accesses(mod, file; skip=(), allow_internal_accesses)
improper_qualified_accesses(mod, file; skip=(), allow_internal_accesses,
file_analysis)
filter!(problematic) do nt
return nt.name ∉ ignore
end
Expand Down Expand Up @@ -429,7 +457,8 @@ function check_all_qualified_accesses_are_public(mod::Module, file=pathof(mod);
# drop unnecessary columns
problematic = NamedTuple{(:name, :location, :value, :accessing_from)}.(problematic)
if !isempty(problematic)
throw(NonPublicQualifiedAccessException(submodule, problematic))
ex = NonPublicQualifiedAccessException(submodule, problematic)
throw ? Base.throw(ex) : return ex
end
end
return nothing
Expand All @@ -442,6 +471,8 @@ end
Checks that neither `mod` nor any of its submodules has self-qualified accesses,
throwing an `SelfQualifiedAccessException` if so, and returning `nothing` otherwise.

If `throw=false`, return the exception instead of throwing.

This can be used in a package's tests, e.g.

```julia
Expand All @@ -466,10 +497,12 @@ Note that if a module is not fully analyzable (e.g. it has dynamic `include` cal
See also: [`improper_qualified_accesses`](@ref) for programmatic access to the same information. Note that while `improper_qualified_accesses` may increase in scope and report other kinds of improper accesses, `check_all_qualified_accesses_are_public` will not.
"""
function check_no_self_qualified_accesses(mod::Module, file=pathof(mod);
ignore::Tuple=())
ignore::Tuple=(), throw=true,
# private undocumented kwarg for hoisting this analysis
file_analysis=Dict())
check_file(file)
for (submodule, problematic) in
improper_qualified_accesses(mod, file; skip=())
improper_qualified_accesses(mod, file; skip=(), file_analysis)
filter!(problematic) do nt
return nt.name ∉ ignore
end
Expand All @@ -482,7 +515,8 @@ function check_no_self_qualified_accesses(mod::Module, file=pathof(mod);
# drop unnecessary columns
problematic = NamedTuple{(:name, :location, :value)}.(problematic)
if !isempty(problematic)
throw(SelfQualifiedAccessException(submodule, problematic))
ex = SelfQualifiedAccessException(submodule, problematic)
throw ? Base.throw(ex) : return ex
end
end
return nothing
Expand All @@ -499,6 +533,8 @@ end
Checks that neither `mod` nor any of its submodules has imports to names via modules other than their owner as determined by `Base.which` (unless the name is public or exported in that module),
throwing an `ExplicitImportsFromNonOwnerException` if so, and returning `nothing` otherwise.

If `throw=false`, return the exception instead of throwing.

This can be used in a package's tests, e.g.

```julia
Expand Down Expand Up @@ -540,7 +576,10 @@ function check_all_explicit_imports_via_owners(mod::Module, file=pathof(mod);
ignore::Tuple=(),
skip::TUPLE_MODULE_PAIRS=get_default_skip_pairs(),
allow_internal_imports=true,
require_submodule_import=false)
require_submodule_import=false,
throw=true,
# private undocumented kwarg for hoisting this analysis
file_analysis=Dict())
check_file(file)
# `strict=false` because unanalyzability doesn't compromise our analysis
# that much, unlike in the stale case (in which we might miss usages of the
Expand All @@ -550,7 +589,8 @@ function check_all_explicit_imports_via_owners(mod::Module, file=pathof(mod);
# throw by default there and not require this function to also throw
# in the exact same cases.
for (submodule, problematic) in
improper_explicit_imports(mod, file; strict=false, skip, allow_internal_imports)
improper_explicit_imports(mod, file; strict=false, skip, allow_internal_imports,
file_analysis)
filter!(problematic) do nt
return nt.name ∉ ignore
end
Expand All @@ -571,7 +611,8 @@ function check_all_explicit_imports_via_owners(mod::Module, file=pathof(mod);
# drop unnecessary columns
problematic = NamedTuple{(:name, :location, :value, :importing_from, :whichmodule)}.(problematic)
if !isempty(problematic)
throw(ExplicitImportsFromNonOwnerException(submodule, problematic))
ex = ExplicitImportsFromNonOwnerException(submodule, problematic)
throw ? Base.throw(ex) : return ex
end
end
return nothing
Expand All @@ -585,6 +626,8 @@ end
Checks that neither `mod` nor any of its submodules has imports to names which are non-public (i.e. not exported, nor declared public on Julia 1.11+)
throwing an `NonPublicExplicitImportsException` if so, and returning `nothing` otherwise.

If `throw=false`, return the exception instead of throwing.

This can be used in a package's tests, e.g.

```julia
Expand Down Expand Up @@ -631,11 +674,15 @@ function check_all_explicit_imports_are_public(mod::Module, file=pathof(mod);
skip::TUPLE_MODULE_PAIRS=(Base => Core,),
from=nothing,
ignore::Tuple=(),
allow_internal_imports=true)
allow_internal_imports=true,
throw=true,
# private undocumented kwarg for hoisting this analysis
file_analysis=Dict())
check_file(file)
for (submodule, problematic) in
# We pass `skip=()` since we will do our own filtering after
improper_explicit_imports(mod, file; strict=false, skip=(), allow_internal_imports)
improper_explicit_imports(mod, file; strict=false, skip=(), allow_internal_imports,
file_analysis)
filter!(problematic) do nt
return nt.name ∉ ignore
end
Expand Down Expand Up @@ -664,7 +711,8 @@ function check_all_explicit_imports_are_public(mod::Module, file=pathof(mod);
# drop unnecessary columns
problematic = NamedTuple{(:name, :location, :value, :importing_from)}.(problematic)
if !isempty(problematic)
throw(NonPublicExplicitImportsException(submodule, problematic))
ex = NonPublicExplicitImportsException(submodule, problematic)
throw ? Base.throw(ex) : return ex
end
end
return nothing
Expand Down
Loading
Loading