API
The standard entrypoint for interactive use is print_explicit_imports
. ExplicitImports.jl API also includes several other functions to provide programmatic access to the information gathered by the package, as well as utilities to use in regression testing.
Detecting implicit imports which could be made explicit
ExplicitImports.print_explicit_imports
— Functionprint_explicit_imports([io::IO=stdout,] mod::Module, file=pathof(mod); skip=(mod, Base, Core),
warn_implicit_imports=true,
warn_improper_explicit_imports=true,
warn_improper_qualified_accesses=true,
report_non_public=VERSION >= v"1.11-",
strict=true)
Runs explicit_imports
and prints the results, along with those of improper_explicit_imports
and improper_qualified_accesses
.
Note that the particular printing may change in future non-breaking releases of ExplicitImports.
Keyword arguments
skip=(mod, Base, Core)
: any names coming from the listed modules (or any submodules thereof) will be skipped. Sincemod
is included by default, implicit imports of names exported from its own submodules will not count by default.warn_improper_explicit_imports=true
: if set, this function will also print information about any "improper" imports of names from other modules.warn_improper_qualified_accesses=true
: if set, this function will also print information about any "improper" qualified accesses to names from other modules.strict=true
: whenstrict
is set, a module will be noted as unanalyzable in the case that the analysis could not be performed accurately, due to e.g. dynamicinclude
statements. Whenstrict=false
, results are returned in all cases, but may be inaccurate.show_locations=false
: whether or not to print locations of where the names are being used.separate_lines=false
: whether or not to print eachusing
statement on a separate line. Automatically occurs whenshow_locations=true
.linewidth=80
: format into lines of up to this length. Set to 0 to indicate one name should be printed per line.report_non_public=VERSION >= v"1.11-"
: report if there are accesses or imports of non-public names (that is, names that are not exported nor marked public). By default, only activates on Julia v1.11+.allow_internal_accesses=true
: if false, reports non-owning or non-public qualified accesses to other modules in the same packageallow_internal_imports=true
: if false, reports non-owning or non-public explicit imports from other modules in the same package
See also check_no_implicit_imports
, check_no_stale_explicit_imports
, check_all_qualified_accesses_via_owners
, and check_all_explicit_imports_via_owners
.
ExplicitImports.explicit_imports
— Functionexplicit_imports(mod::Module, file=pathof(mod); skip=(mod, Base, Core), strict=true)
Returns a nested structure providing information about explicit import statements one could make for each submodule of mod
. This information is structured as a collection of pairs, where the keys are the submodules of mod
(including mod
itself), and the values are NamedTuple
s, with at least the keys name
, source
, exporters
, and location
, showing which names are being used implicitly, which modules they were defined in, which modules they were exported from, and the location of those usages. Additional keys may be added to the NamedTuple
's in the future in non-breaking releases of ExplicitImports.jl.
Arguments
mod::Module
: the module to (recursively) analyze. Often this is a package.file=pathof(mod)
: this should be a path to the source code that contains the modulemod
.- if
mod
is the top-level module of a package,pathof
will be unable to find the code, and a file must be passed which containsmod
(either directly or indirectly throughinclude
s) mod
can be a submodule defined withinfile
, but if two modules have the same name (e.g.X.Y.X
andX
), results may be inaccurate.
- if
Keyword arguments
skip=(mod, Base, Core)
: any names coming from the listed modules (or any submodules thereof) will be skipped. Sincemod
is included by default, implicit imports of names exported from its own submodules will not count by default.strict=true
: whenstrict
is set, results for a module will benothing
in the case that the analysis could not be performed accurately, due to e.g. dynamicinclude
statements. Whenstrict=false
, results are returned in all cases, but may be inaccurate.
If mod
is a package, we can detect the explicit_imports in the package extensions if those extensions are explicitly loaded before calling this function.
For example, consider PackageA
has a weak-dependency on PackageB
and PackageC
in the module PkgBPkgCExt
julia> using ExplicitImports, PackageA
julia> explicit_imports(PackageA) # Only checks for explicit imports in PackageA and its submodules but not in `PkgBPkgCExt`
To check for explicit imports in PkgBPkgCExt
, you can do the following:
julia> using ExplicitImports, PackageA, PackageB, PackageC
julia> explicit_imports(PackageA) # Now checks for explicit imports in PackageA and its submodules and also in `PkgBPkgCExt`
See also print_explicit_imports
to easily compute and print these results, explicit_imports_nonrecursive
for a non-recursive version which ignores submodules, and check_no_implicit_imports
for a version that throws errors, for regression testing.
Detecting "improper" explicit imports
ExplicitImports.improper_explicit_imports
— Functionimproper_explicit_imports(mod::Module, file=pathof(mod); strict=true, skip=(Base => Core,
Compat => Base,
Compat => Core),
allow_internal_imports=true)
Attempts do detect various kinds of "improper" explicit imports taking place in mod
and any submodules of mod
.
Currently detects two classes of issues:
- names which are explicitly imported but unused (stale)
- names which are not public in
mod
- here, public means either exported or declared with the
public
keyword (requires Julia v1.11+) - one particularly egregious type of non-public import is when a name is imported from a module which does not even "own" that name. See the returned fields
importing_from_owns_name
andimporting_from_submodule_owns_name
for two variations on this.
- here, public means either exported or declared with the
The keyword argument allow_internal_imports
determines whether or not "internal" explicit imports to other modules in the same package (or more generally, sharing the same Base.moduleroot
) are reported here. If allow_internal_imports=false
, then even such "internal" explicit imports will be returned.
The keyword argument skip
is expected to be an iterator of importing_from => parent
pairs, where names which are imported from importing_from
but who have an ancestor which is parent
are ignored. By default, imports from Base to names owned by Core are skipped.
This functionality is still in development, so the exact results may change in future non-breaking releases. Read on for the current outputs, what may change, and what will not change (without a breaking release of ExplicitImports.jl).
Returns a nested structure providing information about improper explicit imports to names in other modules. This information is structured as a collection of pairs, where the keys are the submodules of mod
(including mod
itself). Currently, the values are either nothing
or a Vector
of NamedTuple
s with the following keys:
name::Symbol
: the name being importedlocation::String
: the location the import takes placevalue::Any
: the whichname
points to inmod
importing_from::Module
: the module the name is being imported from (e.g. in the exampleusing Foo.X: bar
, this would beX
)whichmodule::Module
: theBase.which
of the objectpublic_import::Bool
: whether or notname
is public or exported inimporting_from
. Checking if a name is markedpublic
requires Julia v1.11+.importing_from_owns_name::Bool
: whether or notimporting_from
matcheswhichmodule
and therefore is considered to directly "own" the nameimporting_from_submodule_owns_name::Bool
: whether or notwhichmodule
is a submodule ofimporting_from
stale::Bool
: whether or not the explicitly imported name is usedinternal_import::Bool
: whether or not the import is "internal", meaning the module it was imported into and the module it was imported from share the sameBase.moduleroot
.
If strict=true
, then returns nothing
if mod
could not be fully analyzed.
In non-breaking releases of ExplicitImports:
- more columns may be added to these rows
- additional rows may be returned which qualify as some other kind of "improper" access
However, the result will be a Tables.jl-compatible row-oriented table (for each module), with at least all of the same columns (or the value will be nothing
if strict=true
and the module could not be fully analyzed).
See also print_explicit_imports
to easily compute and print these results, improper_explicit_imports_nonrecursive
for a non-recursive version which ignores submodules, as well as check_no_stale_explicit_imports
, check_all_explicit_imports_via_owners
, and check_all_explicit_imports_are_public
for specific regression-testing helpers.
Detecting "improper" access of names from other modules
ExplicitImports.improper_qualified_accesses
— Functionimproper_qualified_accesses(mod::Module, file=pathof(mod); skip=(Base => Core,
Compat => Base,
Compat => Core),
allow_internal_accesses=true)
Attempts do detect various kinds of "improper" qualified accesses taking place in mod
and any submodules of mod
.
Currently, only detects cases in which the name is being accessed from a module mod
for which:
name
is not exported frommod
- or
name
is not declared public inmod
(requires Julia v1.11+) - or
name
is "self-qualified": i.e. in the moduleFoo
,Foo.name
is being accessed.
The keyword argument allow_internal_accesses
determines whether or not "internal" qualified accesses to other modules in the same package (or more generally, sharing the same Base.moduleroot
) are reported here. If allow_internal_accesses=false
, then even such "internal" qualified accesses will be returned. Note self-qualified accesses are reported regardless of the setting of allow_internal_accesses
.
The keyword argument skip
is expected to be an iterator of accessing_from => parent
pairs, where names which are accessed from accessing_from
but who have an ancestor parent
are ignored. By default, accesses from Base to names owned by Core are skipped.
This functionality is still in development, so the exact results may change in future non-breaking releases. Read on for the current outputs, what may change, and what will not change (without a breaking release of ExplicitImports.jl).
Returns a nested structure providing information about improper accesses to names in other modules. This information is structured as a collection of pairs, where the keys are the submodules of mod
(including mod
itself). Currently, the values are a Vector
of NamedTuple
s with the following keys:
name::Symbol
: the name being accessedlocation::String
: the location the access takes placevalue::Any
: the whichname
points to inmod
accessing_from::Module
: the module the name is being accessed from (e.g.Module.name
)whichmodule::Module
: theBase.which
of the objectpublic_access::Bool
: whether or notname
is public or exported inaccessing_from
. Checking if a name is markedpublic
requires Julia v1.11+.accessing_from_owns_name::Bool
: whether or notaccessing_from
matcheswhichmodule
and therefore is considered to directly "own" the nameaccessing_from_submodule_owns_name::Bool
: whether or notwhichmodule
is a submodule ofaccessing_from
internal_access::Bool
: whether or not the access is "internal", meaning the module it was accessed in and the module it was accessed from share the sameBase.moduleroot
.self_qualified::Bool
: whether or not the access is "self-qualified", meaning the module it was accessed in and the module it is accessed from are the same module.
In non-breaking releases of ExplicitImports:
- more columns may be added to these rows
- additional rows may be returned which qualify as some other kind of "improper" access
However, the result will be a Tables.jl-compatible row-oriented table (for each module), with at least all of the same columns.
See also print_explicit_imports
to easily compute and print these results, improper_qualified_accesses_nonrecursive
for a non-recursive version which ignores submodules, and the check_
functions check_all_qualified_accesses_via_owners
and check_all_explicit_imports_are_public
for versions that throws errors, for regression testing.
Example
julia> using ExplicitImports
julia> example_path = pkgdir(ExplicitImports, "examples", "qualified.jl");
julia> print(read(example_path, String))
module MyMod
using LinearAlgebra
# sum is in `Base`, so we shouldn't access it from LinearAlgebra:
n = LinearAlgebra.sum([1, 2, 3])
end
julia> include(example_path);
julia> row = improper_qualified_accesses(MyMod, example_path)[1][2][1];
julia> (; row.name, row.accessing_from, row.whichmodule)
(name = :sum, accessing_from = LinearAlgebra, whichmodule = Base)
Checks to use in testing
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
which aims to ensure there are no implicit exports used in the package.
ExplicitImports.check_no_implicit_imports
— Functioncheck_no_implicit_imports(mod::Module, file=pathof(mod); skip=(mod, Base, Core), ignore::Tuple=(),
allow_unanalyzable::Tuple=())
Checks that neither mod
nor any of its submodules is relying on implicit imports, throwing an ImplicitImportsException
if so, and returning nothing
otherwise.
This function can be used in a package's tests, e.g.
@test check_no_implicit_imports(MyPackage) === nothing
Allowing some submodules to be unanalyzable
Pass allow_unanalyzable
as a tuple of submodules which are allowed to be unanalyzable. Any other submodules found to be unanalyzable will result in an UnanalyzableModuleException
being thrown.
These unanalyzable submodules can alternatively be included in ignore
.
Allowing some implicit imports
The skip
keyword argument can be passed to allow implicit imports from some modules (and their submodules). By default, skip
is set to (Base, Core)
. For example:
@test check_no_implicit_imports(MyPackage; skip=(Base, Core, DataFrames)) === nothing
would verify there are no implicit imports from modules other than Base, Core, and DataFrames.
Additionally, the keyword ignore
can be passed to represent a tuple of items to ignore. These can be:
- modules. Any submodule of
mod
matching an element ofignore
is skipped. This can be used to allow the usage of implicit imports in some submodule of your package. - symbols: any implicit import of a name matching an element of
ignore
is ignored (does not throw) symbol => module
pairs. Any implicit import of a name matching that symbol from a module matching the module is ignored.
One can mix and match between these type of ignored elements. For example:
@test check_no_implicit_imports(MyPackage; ignore=(:DataFrame => DataFrames, :ByRow, MySubModule)) === nothing
This would:
- Ignore any implicit import of
DataFrame
from DataFrames - Ignore any implicit import of the name
ByRow
from any module. - Ignore any implicit imports present in
MyPackage
's submoduleMySubModule
but verify there are no other implicit imports.
Next, we have several checks related to detecting "improper" explicit imports. The function check_no_stale_explicit_imports
checks that a module has no "stale" (unused) explicit imports. Next check_all_explicit_imports_via_owners
and check_all_explicit_imports_are_public
provide related checks. check_all_explicit_imports_via_owners
is a weaker check which errors for particularly problematic imports of non-public names, namely those for which the module they are being imported from does not "own" the name (since it was not defined there). The typical scenario here is that the name may be public in some other module, but just happens to be present in the namespace of that module (consider using LinearAlgebra: map
which imports Base's map
function). Next, check_all_explicit_imports_are_public
provides a stricter check that all names being explicitly imported are in fact public in the module they are being imported from, whether or not they are "owned" by that module.
ExplicitImports.check_no_stale_explicit_imports
— Functioncheck_no_stale_explicit_imports(mod::Module, file=pathof(mod); ignore::Tuple=(), allow_unanalyzable::Tuple=())
Checks that neither mod
nor any of its submodules has stale (unused) explicit imports, throwing an StaleImportsException
if so, and returning nothing
otherwise.
This can be used in a package's tests, e.g.
@test check_no_stale_explicit_imports(MyPackage) === nothing
Allowing some submodules to be unanalyzable
Pass allow_unanalyzable
as a tuple of submodules which are allowed to be unanalyzable. Any other submodules found to be unanalyzable will result in an UnanalyzableModuleException
being thrown.
Allowing some stale explicit imports
If ignore
is supplied, it should be a tuple of Symbol
s, representing names that are allowed to be stale explicit imports. For example,
@test check_no_stale_explicit_imports(MyPackage; ignore=(:DataFrame,)) === nothing
would check there were no stale explicit imports besides that of the name DataFrame
.
ExplicitImports.check_all_explicit_imports_via_owners
— Functioncheck_all_explicit_imports_via_owners(mod::Module, file=pathof(mod); ignore::Tuple=(),
require_submodule_import=false,
skip::Tuple{Vararg{Pair{Module, Module}, N}} where N=(Base => Core,
Compat => Base,
Compat => Core)),
allow_internal_imports=true)
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.
This can be used in a package's tests, e.g.
@test check_all_explicit_imports_via_owners(MyPackage) === nothing
Allowing some explicit imports via non-owner modules
The skip
keyword argument can be passed to allow non-owning imports from some modules (and their submodules). One pases a tuple of importing_from => parent
pairs, allowing cases in which a name is being imported from the module importing_from
, but is owned by the module parent
. By default, skip
is set to (Base => Core,)
, meaning that names which are imported from Base but are owned by Core are not flagged.
For example:
@test check_all_explicit_imports_are_public(MyPackage; skip=(Base => Core, DataFrames => PrettyTables)) === nothing
would allow explicitly importing names which are owned by PrettyTables from DataFrames.
If ignore
is supplied, it should be a tuple of Symbol
s, representing names that are allowed to be accessed from non-owner modules. For example,
@test check_all_explicit_imports_via_owners(MyPackage; ignore=(:DataFrame,)) === nothing
would check there were no explicit imports from non-owner modules besides that of the name DataFrame
.
require_submodule_import
If require_submodule_import=true
, then an error will be thrown if the name is imported from a non-owner module even if it is imported from a parent module of the owner module. For example, in June 2024, JSON.parse
is actually defined in the submodule JSON.Parser
and is not declared public inside JSON
, but the name is present within the module JSON
. If require_submodule_import=false
, the default, in this scenario the access using JSON: parse
will not trigger an error, since the name is being accessed by a parent of the owner. If require_submodule_import=false
, then accessing the function as using JSON.Parser: parse
will be required to avoid an error.
non-fully-analyzable modules do not cause exceptions
Note that if a module is not fully analyzable (e.g. it has dynamic include
calls), explicit imports of non-public names which could not be analyzed will be missed. Unlike check_no_stale_explicit_imports
and check_no_implicit_imports
, this function will not throw an UnanalyzableModuleException
in such cases.
See also: improper_explicit_imports
for programmatic access to such imports and the meaning of the keyword argument allow_internal_imports
, and check_all_explicit_imports_are_public
for a stricter version of this check. Note that while improper_explicit_imports
may increase in scope and report other kinds of improper accesses, check_all_explicit_imports_via_owners
will not.
ExplicitImports.check_all_explicit_imports_are_public
— Functioncheck_all_explicit_imports_are_public(mod::Module, file=pathof(mod); ignore::Tuple=(),
skip::Tuple{Vararg{Pair{Module, Module}, N}} where N=(Base => Core,),
allow_internal_imports=true)
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.
This can be used in a package's tests, e.g.
@test check_all_explicit_imports_are_public(MyPackage) === nothing
Allowing some non-public explicit imports
The skip
keyword argument can be passed to allow non-public imports from some modules (and their submodules). One pases a tuple of importing_from => pub
pairs, allowing cases in which a name is being imported from the module importing_from
, but is public in the module pub
. By default, skip
is set to (Base => Core,)
, meaning that names which are imported from Base but are public in Core are not flagged.
For example:
@test check_all_explicit_imports_are_public(MyPackage; skip=(Base => Core, DataFrames => PrettyTables)) === nothing
would allow explicitly importing names which are public in PrettyTables from DataFrames.
If ignore
is supplied, it should be a tuple of Symbol
s, representing names that are allowed to be imported from modules in which they are not public. For example,
@test check_all_explicit_imports_are_public(MyPackage; ignore=(:DataFrame,)) === nothing
would check there were no non-public explicit imports besides that of the name DataFrame
.
non-fully-analyzable modules do not cause exceptions
Note that if a module is not fully analyzable (e.g. it has dynamic include
calls), explicit imports of non-public names which could not be analyzed will be missed. Unlike check_no_stale_explicit_imports
and check_no_implicit_imports
, this function will not throw an UnanalyzableModuleException
in such cases.
See also: improper_explicit_imports
for programmatic access to such imports and the meaning of the keyword argument allow_internal_imports
, and [check_all_explicit_imports_via_owners
] for a weaker version of this check. Note that while improper_explicit_imports
may increase in scope and report other kinds of improper accesses, check_all_explicit_imports_are_public
will not.
Lastly, we have two checks related to detecting "improper" qualified accesses to names, which are analogous to checks related to improper explicit imports. check_all_qualified_accesses_via_owners
checks that all qualified accesses (e.g. usage of names in the form Foo.bar
) are such that the name being accessed is "owned" by the module it is being accessed from (just like check_all_explicit_imports_via_owners
). This would detect, e.g., LinearAlgebra.map
. Likewise, check_all_qualified_accesses_are_public
is a stricter check which verifies all qualified accesses to names are via modules in which that name is public. Additionally, check_no_self_qualified_accesses
checks there are no self-qualified accesses, like accessing Foo.foo
from within the module Foo
.
ExplicitImports.check_all_qualified_accesses_via_owners
— Functioncheck_all_qualified_accesses_via_owners(mod::Module, file=pathof(mod); ignore::Tuple=(),
require_submodule_access=false,
skip::Tuple{Vararg{Pair{Module, Module}, N}} where N=(Base => Core,
Compat => Base,
Compat => Core),
allow_internal_accesses=true)
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.
This can be used in a package's tests, e.g.
@test check_all_qualified_accesses_via_owners(MyPackage) === nothing
Allowing some qualified accesses via non-owner modules
The skip
keyword argument can be passed to allow non-owning accesses via some modules (and their submodules). One pases a tuple of accessing_from => parent
pairs, allowing cases in which a name is being imported from the module accessing_from
, but is owned by the module parent
. By default, skip
is set to (Base => Core,)
, meaning that names which are accessed from Base but are owned by Core are not flagged.
For example:
@test check_all_qualified_accesses_via_owners(MyPackage; skip=(Base => Core, DataFrames => PrettyTables)) === nothing
would allow explicitly accessing names which are owned by PrettyTables from DataFrames.
If ignore
is supplied, it should be a tuple of Symbol
s, representing names that are allowed to be accessed from non-owner modules. For example,
@test check_all_qualified_accesses_via_owners(MyPackage; ignore=(:DataFrame,)) === nothing
would check there were no qualified accesses from non-owner modules besides that of the name DataFrame
.
If require_submodule_access=true
, then an error will be thrown if the name is accessed by a non-owner module even if it is accessed by a parent module of the owner module. For example, in June 2024, JSON.parse
is actually defined in the submodule JSON.Parser
and is not declared public inside JSON
, but the name is present within the module JSON
. If require_submodule_access=false
, the default, in this scenario the access JSON.parse
will not trigger an error, since the name is being accessed by a parent of the owner. If require_submodule_access=false
, then accessing the function as JSON.Parser.parse
will be required to avoid an error.
See also: improper_qualified_accesses
for programmatic access and the meaning of the keyword argument allow_internal_accesses
, and check_all_qualified_accesses_are_public
for a stricter version of this check. Note that while improper_qualified_accesses
may increase in scope and report other kinds of improper accesses, check_all_qualified_accesses_via_owners
will not.
ExplicitImports.check_all_qualified_accesses_are_public
— Functioncheck_all_qualified_accesses_are_public(mod::Module, file=pathof(mod); ignore::Tuple=(),
skip::Tuple{Vararg{Pair{Module, Module}, N}} where N=(Base => Core,),
allow_internal_accesses=true)
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.
This can be used in a package's tests, e.g.
@test check_all_qualified_accesses_are_public(MyPackage) === nothing
Allowing some non-public qualified accesses
The skip
keyword argument can be passed to allow non-public qualified accesses from some modules (and their submodules). One pases a tuple of accessing_from => pub
pairs, allowing cases in which a name is being accessed from the module accessing_from
, but is public in the module pub
. By default, skip
is set to (Base => Core,)
, meaning that names which are accessed from Base but are public in Core are not flagged.
For example:
@test check_all_qualified_accesses_are_public(MyPackage; skip=(Base => Core, DataFrames => PrettyTables)) === nothing
would allow accessing names which are public in PrettyTables from DataFrames.
If ignore
is supplied, it should be a tuple of Symbol
s, representing names that are allowed to be accessed from modules in which they are not public. For example,
@test check_all_qualified_accesses_are_public(MyPackage; ignore=(:DataFrame,)) === nothing
would check there were no non-public qualified accesses besides that of the name DataFrame
.
non-fully-analyzable modules do not cause exceptions
Note that if a module is not fully analyzable (e.g. it has dynamic include
calls), qualified accesess of non-public names which could not be analyzed will be missed. Unlike check_no_stale_explicit_imports
and check_no_implicit_imports
, this function will not throw an UnanalyzableModuleException
in such cases.
See also: improper_qualified_accesses
for programmatic access and the meaning of the keyword argument allow_internal_accesses
, and [check_all_qualified_accesses_via_owners
] for a weaker version of this check. 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.
ExplicitImports.check_no_self_qualified_accesses
— Functioncheck_no_self_qualified_accesses(mod::Module, file=pathof(mod);
ignore::Tuple=())
Checks that neither mod
nor any of its submodules has self-qualified accesses, throwing an SelfQualifiedAccessException
if so, and returning nothing
otherwise.
This can be used in a package's tests, e.g.
@test check_no_self_qualified_accesses(MyPackage) === nothing
Allowing some self-qualified accesses
If ignore
is supplied, it should be a tuple of Symbol
s, representing names that are allowed to be self-qualified. For example,
@test check_no_self_qualified_accesses(MyPackage; ignore=(:foo,)) === nothing
would check there were no self-qualified accesses besides that of the name foo
.
non-fully-analyzable modules do not cause exceptions
Note that if a module is not fully analyzable (e.g. it has dynamic include
calls), qualified accesess of non-public names which could not be analyzed will be missed. Unlike check_no_stale_explicit_imports
and check_no_implicit_imports
, this function will not throw an UnanalyzableModuleException
in such cases.
See also: improper_qualified_accesses
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.
Usage with scripts (such as runtests.jl
)
We also provide a helper function to analyze scripts (rather than modules). If you are using a module in your script (e.g. if your script starts with module
), then use the ordinary print_explicit_imports
function instead. This functionality is somewhat experimental and attempts to filter the relevant names in Main
to those used in your script.
ExplicitImports.print_explicit_imports_script
— Functionprint_explicit_imports_script([io::IO=stdout,] path; skip=(Base, Core), warn_improper_explicit_imports=true)
Analyzes the script located at path
and prints information about reliance on implicit exports as well as any "improper" explicit imports (if warn_improper_explicit_imports=true
).
Note that the particular printing may change in future non-breaking releases of ExplicitImports.
The script (or at least, all imports in the script) must be run before this function can give reliable results, since it relies on introspecting what names are present in Main
.
Keyword arguments
skip=(mod, Base, Core)
: any names coming from the listed modules (or any submodules thereof) will be skipped. Sincemod
is included by default, implicit imports of names exported from its own submodules will not count by default.
Non-recursive variants
The above functions all recurse through submodules of the provided module, providing information about each. Here, we provide non-recursive variants (which in fact power the recursive ones), in case it is useful, perhaps for building other tooling on top of ExplicitImports.jl.
ExplicitImports.explicit_imports_nonrecursive
— Functionexplicit_imports_nonrecursive(mod::Module, file=pathof(mod); skip=(mod, Base, Core), strict=true)
A non-recursive version of explicit_imports
, meaning it only analyzes the module mod
itself, not any of its submodules; see that function for details.
Keyword arguments
skip=(mod, Base, Core)
: any names coming from the listed modules (or any submodules thereof) will be skipped. Sincemod
is included by default, implicit imports of names exported from its own submodules will not count by default.strict=true
: whenstrict=true
, results will benothing
in the case that the analysis could not be performed accurately, due to e.g. dynamicinclude
statements. Whenstrict=false
, results are returned in all cases, but may be inaccurate.
ExplicitImports.improper_qualified_accesses_nonrecursive
— Functionimproper_qualified_accesses_nonrecursive(mod::Module, file=pathof(mod); skip=(Base => Core,
Compat => Base,
Compat => Core),
allow_internal_accesses=true)
A non-recursive version of improper_qualified_accesses
, meaning it only analyzes the module mod
itself, not any of its submodules; see that function for details, including important caveats about stability (outputs may grow in future non-breaking releases of ExplicitImports!).
Example
julia> using ExplicitImports
julia> example_path = pkgdir(ExplicitImports, "examples", "qualified.jl");
julia> print(read(example_path, String))
module MyMod
using LinearAlgebra
# sum is in `Base`, so we shouldn't access it from LinearAlgebra:
n = LinearAlgebra.sum([1, 2, 3])
end
julia> include(example_path);
julia> row = improper_qualified_accesses_nonrecursive(MyMod, example_path)[1];
julia> (; row.name, row.accessing_from, row.whichmodule)
(name = :sum, accessing_from = LinearAlgebra, whichmodule = Base)
ExplicitImports.improper_explicit_imports_nonrecursive
— Functionimproper_explicit_imports_nonrecursive(mod::Module, file=pathof(mod); strict=true, skip=(Base => Core,
Compat => Base,
Compat => Core),
allow_internal_imports=true)
A non-recursive version of improper_explicit_imports
, meaning it only analyzes the module mod
itself, not any of its submodules; see that function for details, including important caveats about stability (outputs may grow in future non-breaking releases of ExplicitImports!).
If strict=true
, then returns nothing
if mod
could not be fully analyzed.