From 3d6cd4e77bef291e2a8f531edfda85f32d9935bc Mon Sep 17 00:00:00 2001 From: "Documenter.jl" Date: Sun, 9 Jun 2024 20:14:24 +0000 Subject: [PATCH] build based on d4da82e --- dev/.documenter-siteinfo.json | 2 +- dev/api/index.html | 14 ++++++++++---- dev/index.html | 4 ++-- dev/internals/index.html | 2 +- dev/objects.inv | Bin 718 -> 728 bytes dev/search_index.js | 2 +- 6 files changed, 15 insertions(+), 9 deletions(-) diff --git a/dev/.documenter-siteinfo.json b/dev/.documenter-siteinfo.json index b961fd6..e34cebc 100644 --- a/dev/.documenter-siteinfo.json +++ b/dev/.documenter-siteinfo.json @@ -1 +1 @@ -{"documenter":{"julia_version":"1.10.4","generation_timestamp":"2024-06-09T12:31:13","documenter_version":"1.4.1"}} \ No newline at end of file +{"documenter":{"julia_version":"1.10.4","generation_timestamp":"2024-06-09T20:14:21","documenter_version":"1.4.1"}} \ No newline at end of file diff --git a/dev/api/index.html b/dev/api/index.html index bc2b996..8b33de0 100644 --- a/dev/api/index.html +++ b/dev/api/index.html @@ -4,11 +4,11 @@ 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

See also check_no_implicit_imports, check_no_stale_explicit_imports, and check_all_qualified_accesses_via_owners.

source
ExplicitImports.explicit_importsFunction
explicit_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 NamedTuples, 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 module mod.
    • 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 contains mod (either directly or indirectly through includes)
    • mod can be a submodule defined within file, but if two modules have the same name (e.g. X.Y.X and X), results may be inaccurate.

Keyword arguments

  • skip=(mod, Base, Core): any names coming from the listed modules (or any submodules thereof) will be skipped. Since mod is included by default, implicit imports of names exported from its own submodules will not count by default.

  • strict=true: when strict is set, results for a module will be nothing in the case that the analysis could not be performed accurately, due to e.g. dynamic include statements. When strict=false, results are returned in all cases, but may be inaccurate.

Note

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
+                        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. Since mod 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: when strict is set, a module will be noted as unanalyzable in the case that the analysis could not be performed accurately, due to e.g. dynamic include statements. When strict=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.

  • linewidth=80: format into lines of up to this length. Set to 0 to indicate one name should be printed per line.

See also check_no_implicit_imports, check_no_stale_explicit_imports, and check_all_qualified_accesses_via_owners.

source
ExplicitImports.explicit_importsFunction
explicit_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 NamedTuples, 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 module mod.
    • 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 contains mod (either directly or indirectly through includes)
    • mod can be a submodule defined within file, but if two modules have the same name (e.g. X.Y.X and X), results may be inaccurate.

Keyword arguments

  • skip=(mod, Base, Core): any names coming from the listed modules (or any submodules thereof) will be skipped. Since mod is included by default, implicit imports of names exported from its own submodules will not count by default.

  • strict=true: when strict is set, results for a module will be nothing in the case that the analysis could not be performed accurately, due to e.g. dynamic include statements. When strict=false, results are returned in all cases, but may be inaccurate.

Note

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.

source

Detecting "improper" explicit imports

ExplicitImports.improper_explicit_importsFunction
improper_explicit_imports(mod::Module, file=pathof(mod); strict=true, skip=(Base => Core,))

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 and importing_from_submodule_owns_name for two variations on this.

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 NamedTuples with the following keys:

  • name::Symbol: the name being imported
  • location::String: the location the access takes place
  • value::Any: the which name points to in mod
  • importing_from::Module: the module the name is being imported from (e.g. in the example using Foo.X: bar, this would be X)
  • whichmodule::Module: the Base.which of the object
  • public_import::Bool: whether or not name is public or exported in importing_from. Checking if a name is marked public requires Julia v1.11+.
  • importing_from_owns_name::Bool: whether or not importing_from matches whichmodule and therefore is considered to directly "own" the name
  • importing_from_submodule_owns_name::Bool: whether or not whichmodule is a submodule of importing_from
  • stale::Bool: whether or not the explicitly imported name is used

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, and improper_explicit_imports_nonrecursive for a non-recursive version which ignores submodules.

source

Detecting "improper" access of names from other modules

ExplicitImports.improper_qualified_accessesFunction
improper_qualified_accesses(mod::Module, file=pathof(mod); skip=(Base => Core,))

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 from mod
  • name is not declared public in mod (requires Julia v1.11+)

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 NamedTuples with the following keys:

  • name::Symbol: the name being accessed
  • location::String: the location the access takes place
  • value::Any: the which name points to in mod
  • accessing_from::Module: the module the name is being accessed from (e.g. Module.name)
  • whichmodule::Module: the Base.which of the object
  • public_access::Bool: whether or not name is public or exported in accessing_from. Checking if a name is marked public requires Julia v1.11+.
  • accessing_from_owns_name::Bool: whether or not accessing_from matches whichmodule and therefore is considered to directly "own" the name
  • accessing_from_submodule_owns_name::Bool: whether or not whichmodule is a submodule of accessing_from

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 check_all_qualified_accesses_via_owners for a version that throws errors, for regression testing.

Example

julia> using ExplicitImports
+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.

source

Detecting "improper" explicit imports

ExplicitImports.improper_explicit_importsFunction
improper_explicit_imports(mod::Module, file=pathof(mod); strict=true, skip=(Base => Core,))

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 and importing_from_submodule_owns_name for two variations on this.

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 NamedTuples with the following keys:

  • name::Symbol: the name being imported
  • location::String: the location the access takes place
  • value::Any: the which name points to in mod
  • importing_from::Module: the module the name is being imported from (e.g. in the example using Foo.X: bar, this would be X)
  • whichmodule::Module: the Base.which of the object
  • public_import::Bool: whether or not name is public or exported in importing_from. Checking if a name is marked public requires Julia v1.11+.
  • importing_from_owns_name::Bool: whether or not importing_from matches whichmodule and therefore is considered to directly "own" the name
  • importing_from_submodule_owns_name::Bool: whether or not whichmodule is a submodule of importing_from
  • stale::Bool: whether or not the explicitly imported name is used

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.

source

Detecting "improper" access of names from other modules

ExplicitImports.improper_qualified_accessesFunction
improper_qualified_accesses(mod::Module, file=pathof(mod); skip=(Base => Core,))

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 from mod
  • name is not declared public in mod (requires Julia v1.11+)

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 NamedTuples with the following keys:

  • name::Symbol: the name being accessed
  • location::String: the location the access takes place
  • value::Any: the which name points to in mod
  • accessing_from::Module: the module the name is being accessed from (e.g. Module.name)
  • whichmodule::Module: the Base.which of the object
  • public_access::Bool: whether or not name is public or exported in accessing_from. Checking if a name is marked public requires Julia v1.11+.
  • accessing_from_owns_name::Bool: whether or not accessing_from matches whichmodule and therefore is considered to directly "own" the name
  • accessing_from_submodule_owns_name::Bool: whether or not whichmodule is a submodule of accessing_from

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 check_all_qualified_accesses_via_owners for a version that throws errors, for regression testing.

Example

julia> using ExplicitImports
 
 julia> example_path = pkgdir(ExplicitImports, "examples", "qualified.jl");
 
@@ -24,7 +24,13 @@
 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)
source

Checks to use in testing

ExplicitImports.jl provides three functions which can be used to regression test that there is no reliance on implicit imports, no stale explicit imports, and no qualified accesses to names from modules other than their owner as determined by Base.which:

ExplicitImports.check_no_implicit_importsFunction
check_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 of ignore 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:

  1. Ignore any implicit import of DataFrame from DataFrames
  2. Ignore any implicit import of the name ByRow from any module.
  3. Ignore any implicit imports present in MyPackage's submodule MySubModule

but verify there are no other implicit imports.

source
ExplicitImports.check_no_stale_explicit_importsFunction
check_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 Symbols, 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.

source
ExplicitImports.check_all_qualified_accesses_via_ownersFunction
check_all_qualified_accesses_via_owners(mod::Module, file=pathof(mod); ignore::Tuple=(), require_submodule_access=false)

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

If ignore is supplied, it should be a tuple of Symbols, 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. Note that while that function may increase in scope and report other kinds of improper accesses, check_all_qualified_accesses_via_owners will not.

source
ExplicitImports.check_all_explicit_imports_via_ownersFunction
check_all_explicit_imports_via_owners(mod::Module, file=pathof(mod); ignore::Tuple=(), allow_unanalyzable::Tuple=(), require_submodule_import=false)

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 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 explicit imports via non-owner modules

If ignore is supplied, it should be a tuple of Symbols, 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.

See also: improper_explicit_imports. Note that while that function may increase in scope and report other kinds of improper accesses, check_all_explicit_imports_via_owners will not.

source

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_scriptFunction
print_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.

Warning

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. Since mod is included by default, implicit imports of names exported from its own submodules will not count by default.
source

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_nonrecursiveFunction
explicit_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. Since mod is included by default, implicit imports of names exported from its own submodules will not count by default.

  • strict=true: when strict=true, results will be nothing in the case that the analysis could not be performed accurately, due to e.g. dynamic include statements. When strict=false, results are returned in all cases, but may be inaccurate.

source
ExplicitImports.improper_qualified_accesses_nonrecursiveFunction
improper_qualified_accesses_nonrecursive(mod::Module, file=pathof(mod); skip=(Base => Core,))

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
+(name = :sum, accessing_from = LinearAlgebra, whichmodule = Base)
source

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_importsFunction
check_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 of ignore 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:

  1. Ignore any implicit import of DataFrame from DataFrames
  2. Ignore any implicit import of the name ByRow from any module.
  3. Ignore any implicit imports present in MyPackage's submodule MySubModule

but verify there are no other implicit imports.

source

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_importsFunction
check_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 Symbols, 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.

source
ExplicitImports.check_all_explicit_imports_via_ownersFunction
check_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,)))

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 Symbols, 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 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.

source
ExplicitImports.check_all_explicit_imports_are_publicFunction
check_all_explicit_imports_are_public(mod::Module, file=pathof(mod); ignore::Tuple=(),
+                                      skip::Tuple{Vararg{Pair{Module, Module}, N}} where N=(Base => Core,))

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 Symbols, 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 [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.

source

Lastly, we have one check related to detecting "improper" qualified accesses to names. 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.

ExplicitImports.check_all_qualified_accesses_via_ownersFunction
check_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,))

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 Symbols, 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. Note that while that function may increase in scope and report other kinds of improper accesses, check_all_qualified_accesses_via_owners will not.

source

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_scriptFunction
print_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.

Warning

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. Since mod is included by default, implicit imports of names exported from its own submodules will not count by default.
source

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_nonrecursiveFunction
explicit_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. Since mod is included by default, implicit imports of names exported from its own submodules will not count by default.

  • strict=true: when strict=true, results will be nothing in the case that the analysis could not be performed accurately, due to e.g. dynamic include statements. When strict=false, results are returned in all cases, but may be inaccurate.

source
ExplicitImports.improper_qualified_accesses_nonrecursiveFunction
improper_qualified_accesses_nonrecursive(mod::Module, file=pathof(mod); skip=(Base => Core,))

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");
 
@@ -40,4 +46,4 @@
 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)
source
ExplicitImports.improper_explicit_imports_nonrecursiveFunction
improper_explicit_imports_nonrecursive(mod::Module, file=pathof(mod); strict=true, skip=(Base => Core,))

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.

source
+(name = :sum, accessing_from = LinearAlgebra, whichmodule = Base)source
ExplicitImports.improper_explicit_imports_nonrecursiveFunction
improper_explicit_imports_nonrecursive(mod::Module, file=pathof(mod); strict=true, skip=(Base => Core,))

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.

source
diff --git a/dev/index.html b/dev/index.html index 8ee9abf..03d94d0 100644 --- a/dev/index.html +++ b/dev/index.html @@ -1,5 +1,5 @@ -Home · ExplicitImports.jl

ExplicitImports

Build Status Coverage

Summary

ExplicitImports.jl helps detect implicit imports and mitigate issues with the alternatives (explicit imports and qualified accesses).

ProblemExampleInteractive detection of issueProgrammatic detectionRegression-testing check
Implicit importsusing LinearAlgebraprint_explicit_importsimplicit_importscheck_no_implicit_imports
Non-owning importusing LinearAlgebra: mapprint_explicit_importsimproper_explicit_importscheck_all_explicit_imports_via_owners
Non-public importusing LinearAlgebra: _svd!print_explicit_imports with report_non_public=trueimproper_explicit_importsnot yet implemented: check_all_explicit_imports_public
Stale importusing LinearAlgebra: svd # unusedprint_explicit_importsimproper_explicit_importscheck_no_stale_explicit_imports
Non-owning accessLinearAlgebra.mapprint_explicit_importsimproper_qualified_accessescheck_all_qualified_accesses_via_owners
Non-public accessLinearAlgebra._svd!print_explicit_imports with report_non_public=trueimproper_qualified_accessesnot yet implemented: check_all_qualified_accesses_public

To understand these examples, note that:

  • svd is an API function of LinearAlgebra
  • map is an API function of Base, which happens to be present in the LinearAlgebra namespace
  • _svd! is a private function of LinearAlgebra

Goals

  • Figure out what implicit imports a Julia module is relying on, in order to make them explicit.
  • Provide tools to help make explicit imports and (more recently) qualified accesses more ergonomic

Terminology

  • implicit import: a name x available in a module due to using XYZ for some package or module XYZ. This name has not been explicitly imported; rather, it is simply available since it is exported by XYZ.
  • explicit import: a name x available in a module due to using XYZ: x or import XYZ: x for some package or module XYZ.
  • qualified access: a name x accessed via XYZ.x

Why

Relying on implicit imports can be problematic, as Base or another package can start exporting that name as well, resulting in a clash. This is a tricky situation because adding a new feature to Base (or a package) and exporting it is not considered a breaking change to its API, but it can cause working code to stop working due to these clashes.

If you've even seen a warning like:

WARNING: both X and Y export "foo"; uses of it in module MyPackage must be qualified

Then this is the kind of clash at issue. When this occurs, the name foo won't point to either package's name, since it is ambiguous which one it should be. However, if the package code is relying on the name foo existing, then there's trouble.

One fix, as the warning suggests, is to qualify the use foo by writing e.g. X.foo or Y.foo. Another option is to explicitly import it, by writing using X: foo instead of just using X.

There are various takes on how problematic this issue is, to what extent this occurs in practice, and to what extent it is worth mitigating. See julia#42080 for some discussion on this.

Personally, I don't think this is always a huge issue, and that it's basically fine for packages to use implicit imports if that is their preferred style and they understand the risk. But I do think this issue is somewhat a "hole" in the semver system as it applies to Julia packages, and I wanted to create some tooling to make it easier to mitigate the issue for package authors who would prefer to not rely on implicit imports.

Example

julia> using ExplicitImports
+Home · ExplicitImports.jl

ExplicitImports

Build Status Coverage

Summary

ExplicitImports.jl helps detect implicit imports and mitigate issues with the alternatives (explicit imports and qualified accesses).

ProblemExampleInteractive detection of issueProgrammatic detectionRegression-testing check
Implicit importsusing LinearAlgebraprint_explicit_importsimplicit_importscheck_no_implicit_imports
Non-owning importusing LinearAlgebra: mapprint_explicit_importsimproper_explicit_importscheck_all_explicit_imports_via_owners
Non-public importusing LinearAlgebra: _svd!print_explicit_imports with report_non_public=trueimproper_explicit_importscheck_all_explicit_imports_are_public
Stale importusing LinearAlgebra: svd # unusedprint_explicit_importsimproper_explicit_importscheck_no_stale_explicit_imports
Non-owning accessLinearAlgebra.mapprint_explicit_importsimproper_qualified_accessescheck_all_qualified_accesses_via_owners
Non-public accessLinearAlgebra._svd!print_explicit_imports with report_non_public=trueimproper_qualified_accessesnot yet implemented: check_all_qualified_accesses_public

To understand these examples, note that:

  • svd is an API function of LinearAlgebra
  • map is an API function of Base, which happens to be present in the LinearAlgebra namespace
  • _svd! is a private function of LinearAlgebra

Goals

  • Figure out what implicit imports a Julia module is relying on, in order to make them explicit.
  • Provide tools to help make explicit imports and (more recently) qualified accesses more ergonomic

Terminology

  • implicit import: a name x available in a module due to using XYZ for some package or module XYZ. This name has not been explicitly imported; rather, it is simply available since it is exported by XYZ.
  • explicit import: a name x available in a module due to using XYZ: x or import XYZ: x for some package or module XYZ.
  • qualified access: a name x accessed via XYZ.x

Why

Relying on implicit imports can be problematic, as Base or another package can start exporting that name as well, resulting in a clash. This is a tricky situation because adding a new feature to Base (or a package) and exporting it is not considered a breaking change to its API, but it can cause working code to stop working due to these clashes.

If you've even seen a warning like:

WARNING: both X and Y export "foo"; uses of it in module MyPackage must be qualified

Then this is the kind of clash at issue. When this occurs, the name foo won't point to either package's name, since it is ambiguous which one it should be. However, if the package code is relying on the name foo existing, then there's trouble.

One fix, as the warning suggests, is to qualify the use foo by writing e.g. X.foo or Y.foo. Another option is to explicitly import it, by writing using X: foo instead of just using X.

There are various takes on how problematic this issue is, to what extent this occurs in practice, and to what extent it is worth mitigating. See julia#42080 for some discussion on this.

Personally, I don't think this is always a huge issue, and that it's basically fine for packages to use implicit imports if that is their preferred style and they understand the risk. But I do think this issue is somewhat a "hole" in the semver system as it applies to Julia packages, and I wanted to create some tooling to make it easier to mitigate the issue for package authors who would prefer to not rely on implicit imports.

Example

julia> using ExplicitImports
 
 julia> print_explicit_imports(ExplicitImports)
 WARNING: both JuliaSyntax and Base export "parse"; uses of it in module ExplicitImports must be qualified
@@ -35,4 +35,4 @@
 Module MathOptInterface.Test._BaseTest could not be accurately analyzed, likely due to dynamic `include` statements. You can pass `strict=false` to attempt to get (possibly inaccurate) results anyway.

Note here we need to pass pkgdir(MathOptInterface) as the second argument, as pathof(MathOptInterface.Test) === nothing and we would get a FileNotFoundException.

If we do pass strict=false, in this case we get

julia> print_explicit_imports(MathOptInterface.Test, pkgdir(MathOptInterface); strict=false)
 Module MathOptInterface.Test is not relying on any implicit imports.
 
-Module MathOptInterface.Test._BaseTest is not relying on any implicit imports.

However, we can't really be sure there isn't a reliance on implicit imports present in the files that we weren't able to scan (or perhaps some stale explicit imports made in those files, or perhaps usages of names explicitly imported in the files we could scan, which would prove those explicit imports are in fact not stale).

Need to load the package/module

This implementation relies on Base.which to introspect which module any given name comes from, and therefore we need to load the module, not just inspect its source code. We can't solely use the source code because implicit imports are implicit – which is part of the criticism of them in the first place, that the source file alone does not tell you where the names come from.

In particular, this means it is hard to convert implicit imports to explicit as a formatting pass, for example.

Given a running language server, however, I think it should be possible to query that for the information needed.

Documentation Index

  • 1An alternate implementation using an AbstractInterpreter (like JET does) might solve this issue (at the cost of increased complexity), and possibly get some handling of tricky scoping situations "for free".
+Module MathOptInterface.Test._BaseTest is not relying on any implicit imports.

However, we can't really be sure there isn't a reliance on implicit imports present in the files that we weren't able to scan (or perhaps some stale explicit imports made in those files, or perhaps usages of names explicitly imported in the files we could scan, which would prove those explicit imports are in fact not stale).

Need to load the package/module

This implementation relies on Base.which to introspect which module any given name comes from, and therefore we need to load the module, not just inspect its source code. We can't solely use the source code because implicit imports are implicit – which is part of the criticism of them in the first place, that the source file alone does not tell you where the names come from.

In particular, this means it is hard to convert implicit imports to explicit as a formatting pass, for example.

Given a running language server, however, I think it should be possible to query that for the information needed.

Documentation Index

  • 1An alternate implementation using an AbstractInterpreter (like JET does) might solve this issue (at the cost of increased complexity), and possibly get some handling of tricky scoping situations "for free".
diff --git a/dev/internals/index.html b/dev/internals/index.html index f902f7e..5de1ddc 100644 --- a/dev/internals/index.html +++ b/dev/internals/index.html @@ -1,2 +1,2 @@ -Dev docs · ExplicitImports.jl

Internal details

Implementation strategy

  1. [DONE hackily] Figure out what names used in the module are being used to refer to bindings in global scope (as opposed to e.g. shadowing globals).
    • We do this by parsing the code (thanks to JuliaSyntax), then reimplementing scoping rules on top of the parse tree
    • This is finicky, but assuming scoping doesn't change, should be robust enough (once the long tail of edge cases are dealt with...)
      • Currently, I don't handle the global keyword, so those may look like local variables and confuse things
    • This means we need access to the raw source code; pathof works well for packages, but for local modules one has to pass the path themselves. Also doesn't seem to work well for stdlibs in the sysimage
  2. [DONE] Figure out what implicit imports are available in the module, and which module they come from
    • done, via a magic ccall from Discourse, and Base.which.
  3. [DONE] Figure out which names have been explicitly imported already
    • Done via parsing

Then we can put this information together to figure out what names are actually being used from other modules, and whose usage could be made explicit, and also which existing explicit imports are not being used.

Internals

ExplicitImports.find_implicit_importsFunction
find_implicit_imports(mod::Module; skip=(mod, Base, Core))

Given a module mod, returns a Dict{Symbol, @NamedTuple{source::Module,exporters::Vector{Module}}} showing names exist in mod's namespace which are available due to implicit exports by other modules. The dict's keys are those names, and the values are the source module that the name comes from, along with the modules which export the same binding that are available in mod due to implicit imports.

In the case of ambiguities (two modules exporting the same name), the name is unavailable in the module, and hence the name will not be present in the dict.

This is powered by Base.which.

source
ExplicitImports.get_names_usedFunction
get_names_used(file) -> FileAnalysis

Figures out which global names are used in file, and what modules they are used within.

Traverses static include statements.

Returns a FileAnalysis object.

source
ExplicitImports.analyze_all_namesFunction
analyze_all_names(file)

Returns a tuple of two items:

  • per_usage_info: a table containing information about each name each time it was used
  • untainted_modules: a set containing modules found and analyzed successfully
source
ExplicitImports.inspect_sessionFunction
ExplicitImports.inspect_session([io::IO=stdout,]; skip=(Base, Core), inner=print_explicit_imports)

Experimental functionality to call inner (defaulting to print_explicit_imports) on each loaded package in the Julia session.

source
ExplicitImports.FileAnalysisType
FileAnalysis

Contains structured analysis results.

Fields

  • perusageinfo::Vector{PerUsageInfo}
  • needs_explicit_import::Set{@NamedTuple{name::Symbol,module_path::Vector{Symbol}, location::String}}
  • unnecessary_explicit_import::Set{@NamedTuple{name::Symbol,module_path::Vector{Symbol}, location::String}}
  • untainted_modules::Set{Vector{Symbol}}: those which were analyzed and do not contain an unanalyzable include
source
+Dev docs · ExplicitImports.jl

Internal details

Implementation strategy

  1. [DONE hackily] Figure out what names used in the module are being used to refer to bindings in global scope (as opposed to e.g. shadowing globals).
    • We do this by parsing the code (thanks to JuliaSyntax), then reimplementing scoping rules on top of the parse tree
    • This is finicky, but assuming scoping doesn't change, should be robust enough (once the long tail of edge cases are dealt with...)
      • Currently, I don't handle the global keyword, so those may look like local variables and confuse things
    • This means we need access to the raw source code; pathof works well for packages, but for local modules one has to pass the path themselves. Also doesn't seem to work well for stdlibs in the sysimage
  2. [DONE] Figure out what implicit imports are available in the module, and which module they come from
    • done, via a magic ccall from Discourse, and Base.which.
  3. [DONE] Figure out which names have been explicitly imported already
    • Done via parsing

Then we can put this information together to figure out what names are actually being used from other modules, and whose usage could be made explicit, and also which existing explicit imports are not being used.

Internals

ExplicitImports.find_implicit_importsFunction
find_implicit_imports(mod::Module; skip=(mod, Base, Core))

Given a module mod, returns a Dict{Symbol, @NamedTuple{source::Module,exporters::Vector{Module}}} showing names exist in mod's namespace which are available due to implicit exports by other modules. The dict's keys are those names, and the values are the source module that the name comes from, along with the modules which export the same binding that are available in mod due to implicit imports.

In the case of ambiguities (two modules exporting the same name), the name is unavailable in the module, and hence the name will not be present in the dict.

This is powered by Base.which.

source
ExplicitImports.get_names_usedFunction
get_names_used(file) -> FileAnalysis

Figures out which global names are used in file, and what modules they are used within.

Traverses static include statements.

Returns a FileAnalysis object.

source
ExplicitImports.analyze_all_namesFunction
analyze_all_names(file)

Returns a tuple of two items:

  • per_usage_info: a table containing information about each name each time it was used
  • untainted_modules: a set containing modules found and analyzed successfully
source
ExplicitImports.inspect_sessionFunction
ExplicitImports.inspect_session([io::IO=stdout,]; skip=(Base, Core), inner=print_explicit_imports)

Experimental functionality to call inner (defaulting to print_explicit_imports) on each loaded package in the Julia session.

source
ExplicitImports.FileAnalysisType
FileAnalysis

Contains structured analysis results.

Fields

  • perusageinfo::Vector{PerUsageInfo}
  • needs_explicit_import::Set{@NamedTuple{name::Symbol,module_path::Vector{Symbol}, location::String}}
  • unnecessary_explicit_import::Set{@NamedTuple{name::Symbol,module_path::Vector{Symbol}, location::String}}
  • untainted_modules::Set{Vector{Symbol}}: those which were analyzed and do not contain an unanalyzable include
source
diff --git a/dev/objects.inv b/dev/objects.inv index 408da440b44e640c582f2598719527afab38c513..5069931426932d0f0a99ca2576ab39f958e49169 100644 GIT binary patch delta 606 zcmV-k0-^oR1=t0Ug@3J8O>^5I5WVYH?4^Q=40+X{Ww)dg+H!1??xHv;e@z;{zp#w$5K4BK0{|G5B51zFO9GO0aT z&6OMD`omjzpbdmmmwj22`0daetR11$=*fAhSCOaql2nTD?tdCuFn9w29OR$_XAk5_ zG%LK4Wo1o8_6oTW-Fb2r?+m=JTW#mZzxoTo|%|mItPQ z$b*Ilkp~?w`(ZR%618=5L%88qa_y&*yM|{_NYIi05LiheeX}suJKV0NSfent%5h0p z@|=*KTF_*O!GA^4f=&yf7DO#r5;R(nw6aClUChaIsb1ti^ge`0Q0So9FUMDeTj|&6 zgq2mu`@O3}-rS+Bb<<{d^Qb;dBs@W#%|A_$XY;AdCKC z=#kCnj4==~+1D<_qK$Bqg0nw0SF(_lv-XORW7_CpxF6MBQEL&UMtQu{6V02zxKb>c zVVxPqEwQRzhQm|EUP_$G>rGuM@J%YZ596?NGIIQ`B6u|=$I|`Z1>8u^%vO^<*+(ID z&eFTFr&kC^OttxFb4+G$4wVfNF7CvE`F!&>9#ZWZvA8zZ9LcKk8ZZ>EU4QZ?Mn&Kin-uaD+!U5C8xG delta 596 zcmV-a0;~Pl16u`tmCckQRg$vf zetjuQl$2P0<|YXHv;ezzzZ%B1#W zHCJwo>p$MXBdsBPb=lVyi9Zg#!P*f@jh>vBdKr0&KaolmK7U+83kGi>fP)-#;Ovn+ ziRKBfWLa5Lk-b7L1UZMMK+Q{XSXfh{@hh@en!HxzCW;J7v-vz}g~wCWdM*qupyh!n zAo8H$LF7Tl%YGV-mPBoxTobOjm0bHNbJy?+3JE&O9||ieq;DRK^$s^HDOM;1f!j1GR zbi&Fi`2OhX;F~+NwQk1jZlBf7gy9M5Z2ozIJez-k2!;j>9T;LA;10%hvPFnaC27yS z3@vxwa!Xl>DQZr2Pq=MW0>{)?VQI!v6;-k|)ckMjbo= diff --git a/dev/search_index.js b/dev/search_index.js index fe77da1..bc621e1 100644 --- a/dev/search_index.js +++ b/dev/search_index.js @@ -1,3 +1,3 @@ var documenterSearchIndex = {"docs": -[{"location":"api/#API","page":"API reference","title":"API","text":"","category":"section"},{"location":"api/","page":"API reference","title":"API reference","text":"The main 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.","category":"page"},{"location":"api/#Detecting-implicit-imports-which-could-be-made-explicit","page":"API reference","title":"Detecting implicit imports which could be made explicit","text":"","category":"section"},{"location":"api/","page":"API reference","title":"API reference","text":"print_explicit_imports\nexplicit_imports","category":"page"},{"location":"api/#ExplicitImports.print_explicit_imports","page":"API reference","title":"ExplicitImports.print_explicit_imports","text":"print_explicit_imports([io::IO=stdout,] mod::Module, file=pathof(mod); skip=(mod, Base, Core),\n warn_implicit_imports=true,\n warn_improper_explicit_imports=true,\n warn_improper_qualified_accesses=true,\n report_non_public=VERSION >= v\"1.11-\",\n strict=true)\n\nRuns explicit_imports and prints the results, along with those of improper_explicit_imports and improper_qualified_accesses.\n\nNote that the particular printing may change in future non-breaking releases of ExplicitImports.\n\nKeyword arguments\n\nskip=(mod, Base, Core): any names coming from the listed modules (or any submodules thereof) will be skipped. Since mod is included by default, implicit imports of names exported from its own submodules will not count by default.\nwarn_improper_explicit_imports=true: if set, this function will also print information about any \"improper\" imports of names from other modules.\nwarn_improper_qualified_accesses=true: if set, this function will also print information about any \"improper\" qualified accesses to names from other modules.\nstrict=true: when strict is set, a module will be noted as unanalyzable in the case that the analysis could not be performed accurately, due to e.g. dynamic include statements. When strict=false, results are returned in all cases, but may be inaccurate.\nshow_locations=false: whether or not to print locations of where the names are being used.\nlinewidth=80: format into lines of up to this length. Set to 0 to indicate one name should be printed per line.\n\nSee also check_no_implicit_imports, check_no_stale_explicit_imports, and check_all_qualified_accesses_via_owners.\n\n\n\n\n\n","category":"function"},{"location":"api/#ExplicitImports.explicit_imports","page":"API reference","title":"ExplicitImports.explicit_imports","text":"explicit_imports(mod::Module, file=pathof(mod); skip=(mod, Base, Core), strict=true)\n\nReturns 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 NamedTuples, 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.\n\nArguments\n\nmod::Module: the module to (recursively) analyze. Often this is a package.\nfile=pathof(mod): this should be a path to the source code that contains the module mod.\nif mod is the top-level module of a package, pathof will be unable to find the code, and a file must be passed which contains mod (either directly or indirectly through includes)\nmod can be a submodule defined within file, but if two modules have the same name (e.g. X.Y.X and X), results may be inaccurate.\n\nKeyword arguments\n\nskip=(mod, Base, Core): any names coming from the listed modules (or any submodules thereof) will be skipped. Since mod is included by default, implicit imports of names exported from its own submodules will not count by default.\nstrict=true: when strict is set, results for a module will be nothing in the case that the analysis could not be performed accurately, due to e.g. dynamic include statements. When strict=false, results are returned in all cases, but may be inaccurate.\n\nnote: Note\nIf 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 PkgBPkgCExtjulia> using ExplicitImports, PackageA\n\njulia> 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\n\njulia> explicit_imports(PackageA) # Now checks for explicit imports in PackageA and its submodules and also in `PkgBPkgCExt`\n\nSee 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.\n\n\n\n\n\n","category":"function"},{"location":"api/#Detecting-\"improper\"-explicit-imports","page":"API reference","title":"Detecting \"improper\" explicit imports","text":"","category":"section"},{"location":"api/","page":"API reference","title":"API reference","text":"improper_explicit_imports","category":"page"},{"location":"api/#ExplicitImports.improper_explicit_imports","page":"API reference","title":"ExplicitImports.improper_explicit_imports","text":"improper_explicit_imports(mod::Module, file=pathof(mod); strict=true, skip=(Base => Core,))\n\nAttempts do detect various kinds of \"improper\" explicit imports taking place in mod and any submodules of mod.\n\nCurrently detects two classes of issues:\n\nnames which are explicitly imported but unused (stale)\nnames which are not public in mod\nhere, public means either exported or declared with the public keyword (requires Julia v1.11+)\none 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 and importing_from_submodule_owns_name for two variations on this.\n\nThe 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.\n\nThis 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).\n\nReturns 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 NamedTuples with the following keys:\n\nname::Symbol: the name being imported\nlocation::String: the location the access takes place\nvalue::Any: the which name points to in mod\nimporting_from::Module: the module the name is being imported from (e.g. in the example using Foo.X: bar, this would be X)\nwhichmodule::Module: the Base.which of the object\npublic_import::Bool: whether or not name is public or exported in importing_from. Checking if a name is marked public requires Julia v1.11+.\nimporting_from_owns_name::Bool: whether or not importing_from matches whichmodule and therefore is considered to directly \"own\" the name\nimporting_from_submodule_owns_name::Bool: whether or not whichmodule is a submodule of importing_from\nstale::Bool: whether or not the explicitly imported name is used\n\nIf strict=true, then returns nothing if mod could not be fully analyzed.\n\nIn non-breaking releases of ExplicitImports:\n\nmore columns may be added to these rows\nadditional rows may be returned which qualify as some other kind of \"improper\" access\n\nHowever, 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).\n\nSee also print_explicit_imports to easily compute and print these results, and improper_explicit_imports_nonrecursive for a non-recursive version which ignores submodules.\n\n\n\n\n\n","category":"function"},{"location":"api/#Detecting-\"improper\"-access-of-names-from-other-modules","page":"API reference","title":"Detecting \"improper\" access of names from other modules","text":"","category":"section"},{"location":"api/","page":"API reference","title":"API reference","text":"improper_qualified_accesses","category":"page"},{"location":"api/#ExplicitImports.improper_qualified_accesses","page":"API reference","title":"ExplicitImports.improper_qualified_accesses","text":"improper_qualified_accesses(mod::Module, file=pathof(mod); skip=(Base => Core,))\n\nAttempts do detect various kinds of \"improper\" qualified accesses taking place in mod and any submodules of mod.\n\nCurrently, only detects cases in which the name is being accessed from a module mod for which:\n\nname is not exported from mod\nname is not declared public in mod (requires Julia v1.11+)\n\nThe 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.\n\nThis 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).\n\nReturns 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 NamedTuples with the following keys:\n\nname::Symbol: the name being accessed\nlocation::String: the location the access takes place\nvalue::Any: the which name points to in mod\naccessing_from::Module: the module the name is being accessed from (e.g. Module.name)\nwhichmodule::Module: the Base.which of the object\npublic_access::Bool: whether or not name is public or exported in accessing_from. Checking if a name is marked public requires Julia v1.11+.\naccessing_from_owns_name::Bool: whether or not accessing_from matches whichmodule and therefore is considered to directly \"own\" the name\naccessing_from_submodule_owns_name::Bool: whether or not whichmodule is a submodule of accessing_from\n\nIn non-breaking releases of ExplicitImports:\n\nmore columns may be added to these rows\nadditional rows may be returned which qualify as some other kind of \"improper\" access\n\nHowever, the result will be a Tables.jl-compatible row-oriented table (for each module), with at least all of the same columns.\n\nSee also print_explicit_imports to easily compute and print these results, improper_qualified_accesses_nonrecursive for a non-recursive version which ignores submodules, and check_all_qualified_accesses_via_owners for a version that throws errors, for regression testing.\n\nExample\n\njulia> using ExplicitImports\n\njulia> example_path = pkgdir(ExplicitImports, \"examples\", \"qualified.jl\");\n\njulia> print(read(example_path, String))\nmodule MyMod\nusing LinearAlgebra\n# sum is in `Base`, so we shouldn't access it from LinearAlgebra:\nn = LinearAlgebra.sum([1, 2, 3])\nend\n\njulia> include(example_path);\n\njulia> row = improper_qualified_accesses(MyMod, example_path)[1][2][1];\n\njulia> (; row.name, row.accessing_from, row.whichmodule)\n(name = :sum, accessing_from = LinearAlgebra, whichmodule = Base)\n\n\n\n\n\n","category":"function"},{"location":"api/#Checks-to-use-in-testing","page":"API reference","title":"Checks to use in testing","text":"","category":"section"},{"location":"api/","page":"API reference","title":"API reference","text":"ExplicitImports.jl provides three functions which can be used to regression test that there is no reliance on implicit imports, no stale explicit imports, and no qualified accesses to names from modules other than their owner as determined by Base.which:","category":"page"},{"location":"api/","page":"API reference","title":"API reference","text":"check_no_implicit_imports\ncheck_no_stale_explicit_imports\ncheck_all_qualified_accesses_via_owners\ncheck_all_explicit_imports_via_owners","category":"page"},{"location":"api/#ExplicitImports.check_no_implicit_imports","page":"API reference","title":"ExplicitImports.check_no_implicit_imports","text":"check_no_implicit_imports(mod::Module, file=pathof(mod); skip=(mod, Base, Core), ignore::Tuple=(), allow_unanalyzable::Tuple=())\n\nChecks that neither mod nor any of its submodules is relying on implicit imports, throwing an ImplicitImportsException if so, and returning nothing otherwise.\n\nThis function can be used in a package's tests, e.g.\n\n@test check_no_implicit_imports(MyPackage) === nothing\n\nAllowing some submodules to be unanalyzable\n\nPass 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.\n\nThese unanalyzable submodules can alternatively be included in ignore.\n\nAllowing some implicit imports\n\nThe 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:\n\n@test check_no_implicit_imports(MyPackage; skip=(Base, Core, DataFrames)) === nothing\n\nwould verify there are no implicit imports from modules other than Base, Core, and DataFrames.\n\nAdditionally, the keyword ignore can be passed to represent a tuple of items to ignore. These can be:\n\nmodules. Any submodule of mod matching an element of ignore is skipped. This can be used to allow the usage of implicit imports in some submodule of your package.\nsymbols: any implicit import of a name matching an element of ignore is ignored (does not throw)\nsymbol => module pairs. Any implicit import of a name matching that symbol from a module matching the module is ignored.\n\nOne can mix and match between these type of ignored elements. For example:\n\n@test check_no_implicit_imports(MyPackage; ignore=(:DataFrame => DataFrames, :ByRow, MySubModule)) === nothing\n\nThis would:\n\nIgnore any implicit import of DataFrame from DataFrames\nIgnore any implicit import of the name ByRow from any module.\nIgnore any implicit imports present in MyPackage's submodule MySubModule\n\nbut verify there are no other implicit imports.\n\n\n\n\n\n","category":"function"},{"location":"api/#ExplicitImports.check_no_stale_explicit_imports","page":"API reference","title":"ExplicitImports.check_no_stale_explicit_imports","text":"check_no_stale_explicit_imports(mod::Module, file=pathof(mod); ignore::Tuple=(), allow_unanalyzable::Tuple=())\n\nChecks that neither mod nor any of its submodules has stale (unused) explicit imports, throwing an StaleImportsException if so, and returning nothing otherwise.\n\nThis can be used in a package's tests, e.g.\n\n@test check_no_stale_explicit_imports(MyPackage) === nothing\n\nAllowing some submodules to be unanalyzable\n\nPass 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.\n\nAllowing some stale explicit imports\n\nIf ignore is supplied, it should be a tuple of Symbols, representing names that are allowed to be stale explicit imports. For example,\n\n@test check_no_stale_explicit_imports(MyPackage; ignore=(:DataFrame,)) === nothing\n\nwould check there were no stale explicit imports besides that of the name DataFrame.\n\n\n\n\n\n","category":"function"},{"location":"api/#ExplicitImports.check_all_qualified_accesses_via_owners","page":"API reference","title":"ExplicitImports.check_all_qualified_accesses_via_owners","text":"check_all_qualified_accesses_via_owners(mod::Module, file=pathof(mod); ignore::Tuple=(), require_submodule_access=false)\n\nChecks 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.\n\nThis can be used in a package's tests, e.g.\n\n@test check_all_qualified_accesses_via_owners(MyPackage) === nothing\n\nAllowing some qualified accesses via non-owner modules\n\nIf ignore is supplied, it should be a tuple of Symbols, representing names that are allowed to be accessed from non-owner modules. For example,\n\n@test check_all_qualified_accesses_via_owners(MyPackage; ignore=(:DataFrame,)) === nothing\n\nwould check there were no qualified accesses from non-owner modules besides that of the name DataFrame.\n\nIf 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.\n\nSee also: improper_qualified_accesses. Note that while that function may increase in scope and report other kinds of improper accesses, check_all_qualified_accesses_via_owners will not.\n\n\n\n\n\n","category":"function"},{"location":"api/#ExplicitImports.check_all_explicit_imports_via_owners","page":"API reference","title":"ExplicitImports.check_all_explicit_imports_via_owners","text":"check_all_explicit_imports_via_owners(mod::Module, file=pathof(mod); ignore::Tuple=(), allow_unanalyzable::Tuple=(), require_submodule_import=false)\n\nChecks 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.\n\nThis can be used in a package's tests, e.g.\n\n@test check_all_explicit_imports_via_owners(MyPackage) === nothing\n\nAllowing some submodules to be unanalyzable\n\nPass 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.\n\nAllowing some explicit imports via non-owner modules\n\nIf ignore is supplied, it should be a tuple of Symbols, representing names that are allowed to be accessed from non-owner modules. For example,\n\n@test check_all_explicit_imports_via_owners(MyPackage; ignore=(:DataFrame,)) === nothing\n\nwould check there were no explicit imports from non-owner modules besides that of the name DataFrame.\n\nrequire_submodule_import\n\nIf 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.\n\nSee also: improper_explicit_imports. Note that while that function may increase in scope and report other kinds of improper accesses, check_all_explicit_imports_via_owners will not.\n\n\n\n\n\n","category":"function"},{"location":"api/#Usage-with-scripts-(such-as-runtests.jl)","page":"API reference","title":"Usage with scripts (such as runtests.jl)","text":"","category":"section"},{"location":"api/","page":"API reference","title":"API reference","text":"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.","category":"page"},{"location":"api/","page":"API reference","title":"API reference","text":"print_explicit_imports_script","category":"page"},{"location":"api/#ExplicitImports.print_explicit_imports_script","page":"API reference","title":"ExplicitImports.print_explicit_imports_script","text":"print_explicit_imports_script([io::IO=stdout,] path; skip=(Base, Core), warn_improper_explicit_imports=true)\n\nAnalyzes 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).\n\nNote that the particular printing may change in future non-breaking releases of ExplicitImports.\n\nwarning: Warning\n\n\nThe 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.\n\nKeyword arguments\n\nskip=(mod, Base, Core): any names coming from the listed modules (or any submodules thereof) will be skipped. Since mod is included by default, implicit imports of names exported from its own submodules will not count by default.\n\n\n\n\n\n","category":"function"},{"location":"api/#Non-recursive-variants","page":"API reference","title":"Non-recursive variants","text":"","category":"section"},{"location":"api/","page":"API reference","title":"API reference","text":"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.","category":"page"},{"location":"api/","page":"API reference","title":"API reference","text":"explicit_imports_nonrecursive\nimproper_qualified_accesses_nonrecursive\nimproper_explicit_imports_nonrecursive","category":"page"},{"location":"api/#ExplicitImports.explicit_imports_nonrecursive","page":"API reference","title":"ExplicitImports.explicit_imports_nonrecursive","text":"explicit_imports_nonrecursive(mod::Module, file=pathof(mod); skip=(mod, Base, Core), strict=true)\n\nA non-recursive version of explicit_imports, meaning it only analyzes the module mod itself, not any of its submodules; see that function for details.\n\nKeyword arguments\n\nskip=(mod, Base, Core): any names coming from the listed modules (or any submodules thereof) will be skipped. Since mod is included by default, implicit imports of names exported from its own submodules will not count by default.\nstrict=true: when strict=true, results will be nothing in the case that the analysis could not be performed accurately, due to e.g. dynamic include statements. When strict=false, results are returned in all cases, but may be inaccurate.\n\n\n\n\n\n","category":"function"},{"location":"api/#ExplicitImports.improper_qualified_accesses_nonrecursive","page":"API reference","title":"ExplicitImports.improper_qualified_accesses_nonrecursive","text":"improper_qualified_accesses_nonrecursive(mod::Module, file=pathof(mod); skip=(Base => Core,))\n\nA 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!).\n\nExample\n\njulia> using ExplicitImports\n\njulia> example_path = pkgdir(ExplicitImports, \"examples\", \"qualified.jl\");\n\njulia> print(read(example_path, String))\nmodule MyMod\nusing LinearAlgebra\n# sum is in `Base`, so we shouldn't access it from LinearAlgebra:\nn = LinearAlgebra.sum([1, 2, 3])\nend\n\njulia> include(example_path);\n\njulia> row = improper_qualified_accesses_nonrecursive(MyMod, example_path)[1];\n\njulia> (; row.name, row.accessing_from, row.whichmodule)\n(name = :sum, accessing_from = LinearAlgebra, whichmodule = Base)\n\n\n\n\n\n","category":"function"},{"location":"api/#ExplicitImports.improper_explicit_imports_nonrecursive","page":"API reference","title":"ExplicitImports.improper_explicit_imports_nonrecursive","text":"improper_explicit_imports_nonrecursive(mod::Module, file=pathof(mod); strict=true, skip=(Base => Core,))\n\nA 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!).\n\nIf strict=true, then returns nothing if mod could not be fully analyzed.\n\n\n\n\n\n","category":"function"},{"location":"","page":"Home","title":"Home","text":"CurrentModule = ExplicitImports","category":"page"},{"location":"","page":"Home","title":"Home","text":"using ExplicitImports, Markdown\ncontents = read(joinpath(pkgdir(ExplicitImports), \"README.md\"), String)\ncontents = replace(contents, \"[![stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://ericphanson.github.io/ExplicitImports.jl/stable/)\" => \"\")\nMarkdown.parse(contents)","category":"page"},{"location":"#Documentation-Index","page":"Home","title":"Documentation Index","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"","category":"page"},{"location":"internals/#Internal-details","page":"Dev docs","title":"Internal details","text":"","category":"section"},{"location":"internals/#Implementation-strategy","page":"Dev docs","title":"Implementation strategy","text":"","category":"section"},{"location":"internals/","page":"Dev docs","title":"Dev docs","text":"[DONE hackily] Figure out what names used in the module are being used to refer to bindings in global scope (as opposed to e.g. shadowing globals).\nWe do this by parsing the code (thanks to JuliaSyntax), then reimplementing scoping rules on top of the parse tree\nThis is finicky, but assuming scoping doesn't change, should be robust enough (once the long tail of edge cases are dealt with...)\nCurrently, I don't handle the global keyword, so those may look like local variables and confuse things\nThis means we need access to the raw source code; pathof works well for packages, but for local modules one has to pass the path themselves. Also doesn't seem to work well for stdlibs in the sysimage\n[DONE] Figure out what implicit imports are available in the module, and which module they come from\ndone, via a magic ccall from Discourse, and Base.which.\n[DONE] Figure out which names have been explicitly imported already\nDone via parsing","category":"page"},{"location":"internals/","page":"Dev docs","title":"Dev docs","text":"Then we can put this information together to figure out what names are actually being used from other modules, and whose usage could be made explicit, and also which existing explicit imports are not being used.","category":"page"},{"location":"internals/#Internals","page":"Dev docs","title":"Internals","text":"","category":"section"},{"location":"internals/","page":"Dev docs","title":"Dev docs","text":"ExplicitImports.find_implicit_imports\nExplicitImports.get_names_used\nExplicitImports.analyze_all_names\nExplicitImports.inspect_session\nExplicitImports.FileAnalysis","category":"page"},{"location":"internals/#ExplicitImports.find_implicit_imports","page":"Dev docs","title":"ExplicitImports.find_implicit_imports","text":"find_implicit_imports(mod::Module; skip=(mod, Base, Core))\n\nGiven a module mod, returns a Dict{Symbol, @NamedTuple{source::Module,exporters::Vector{Module}}} showing names exist in mod's namespace which are available due to implicit exports by other modules. The dict's keys are those names, and the values are the source module that the name comes from, along with the modules which export the same binding that are available in mod due to implicit imports.\n\nIn the case of ambiguities (two modules exporting the same name), the name is unavailable in the module, and hence the name will not be present in the dict.\n\nThis is powered by Base.which.\n\n\n\n\n\n","category":"function"},{"location":"internals/#ExplicitImports.get_names_used","page":"Dev docs","title":"ExplicitImports.get_names_used","text":"get_names_used(file) -> FileAnalysis\n\nFigures out which global names are used in file, and what modules they are used within.\n\nTraverses static include statements.\n\nReturns a FileAnalysis object.\n\n\n\n\n\n","category":"function"},{"location":"internals/#ExplicitImports.analyze_all_names","page":"Dev docs","title":"ExplicitImports.analyze_all_names","text":"analyze_all_names(file)\n\nReturns a tuple of two items:\n\nper_usage_info: a table containing information about each name each time it was used\nuntainted_modules: a set containing modules found and analyzed successfully\n\n\n\n\n\n","category":"function"},{"location":"internals/#ExplicitImports.inspect_session","page":"Dev docs","title":"ExplicitImports.inspect_session","text":"ExplicitImports.inspect_session([io::IO=stdout,]; skip=(Base, Core), inner=print_explicit_imports)\n\nExperimental functionality to call inner (defaulting to print_explicit_imports) on each loaded package in the Julia session.\n\n\n\n\n\n","category":"function"},{"location":"internals/#ExplicitImports.FileAnalysis","page":"Dev docs","title":"ExplicitImports.FileAnalysis","text":"FileAnalysis\n\nContains structured analysis results.\n\nFields\n\nperusageinfo::Vector{PerUsageInfo}\nneeds_explicit_import::Set{@NamedTuple{name::Symbol,module_path::Vector{Symbol}, location::String}}\nunnecessary_explicit_import::Set{@NamedTuple{name::Symbol,module_path::Vector{Symbol}, location::String}}\nuntainted_modules::Set{Vector{Symbol}}: those which were analyzed and do not contain an unanalyzable include\n\n\n\n\n\n","category":"type"}] +[{"location":"api/#API","page":"API reference","title":"API","text":"","category":"section"},{"location":"api/","page":"API reference","title":"API reference","text":"The main 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.","category":"page"},{"location":"api/#Detecting-implicit-imports-which-could-be-made-explicit","page":"API reference","title":"Detecting implicit imports which could be made explicit","text":"","category":"section"},{"location":"api/","page":"API reference","title":"API reference","text":"print_explicit_imports\nexplicit_imports","category":"page"},{"location":"api/#ExplicitImports.print_explicit_imports","page":"API reference","title":"ExplicitImports.print_explicit_imports","text":"print_explicit_imports([io::IO=stdout,] mod::Module, file=pathof(mod); skip=(mod, Base, Core),\n warn_implicit_imports=true,\n warn_improper_explicit_imports=true,\n warn_improper_qualified_accesses=true,\n report_non_public=VERSION >= v\"1.11-\",\n strict=true)\n\nRuns explicit_imports and prints the results, along with those of improper_explicit_imports and improper_qualified_accesses.\n\nNote that the particular printing may change in future non-breaking releases of ExplicitImports.\n\nKeyword arguments\n\nskip=(mod, Base, Core): any names coming from the listed modules (or any submodules thereof) will be skipped. Since mod is included by default, implicit imports of names exported from its own submodules will not count by default.\nwarn_improper_explicit_imports=true: if set, this function will also print information about any \"improper\" imports of names from other modules.\nwarn_improper_qualified_accesses=true: if set, this function will also print information about any \"improper\" qualified accesses to names from other modules.\nstrict=true: when strict is set, a module will be noted as unanalyzable in the case that the analysis could not be performed accurately, due to e.g. dynamic include statements. When strict=false, results are returned in all cases, but may be inaccurate.\nshow_locations=false: whether or not to print locations of where the names are being used.\nlinewidth=80: format into lines of up to this length. Set to 0 to indicate one name should be printed per line.\n\nSee also check_no_implicit_imports, check_no_stale_explicit_imports, and check_all_qualified_accesses_via_owners.\n\n\n\n\n\n","category":"function"},{"location":"api/#ExplicitImports.explicit_imports","page":"API reference","title":"ExplicitImports.explicit_imports","text":"explicit_imports(mod::Module, file=pathof(mod); skip=(mod, Base, Core), strict=true)\n\nReturns 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 NamedTuples, 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.\n\nArguments\n\nmod::Module: the module to (recursively) analyze. Often this is a package.\nfile=pathof(mod): this should be a path to the source code that contains the module mod.\nif mod is the top-level module of a package, pathof will be unable to find the code, and a file must be passed which contains mod (either directly or indirectly through includes)\nmod can be a submodule defined within file, but if two modules have the same name (e.g. X.Y.X and X), results may be inaccurate.\n\nKeyword arguments\n\nskip=(mod, Base, Core): any names coming from the listed modules (or any submodules thereof) will be skipped. Since mod is included by default, implicit imports of names exported from its own submodules will not count by default.\nstrict=true: when strict is set, results for a module will be nothing in the case that the analysis could not be performed accurately, due to e.g. dynamic include statements. When strict=false, results are returned in all cases, but may be inaccurate.\n\nnote: Note\nIf 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 PkgBPkgCExtjulia> using ExplicitImports, PackageA\n\njulia> 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\n\njulia> explicit_imports(PackageA) # Now checks for explicit imports in PackageA and its submodules and also in `PkgBPkgCExt`\n\nSee 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.\n\n\n\n\n\n","category":"function"},{"location":"api/#Detecting-\"improper\"-explicit-imports","page":"API reference","title":"Detecting \"improper\" explicit imports","text":"","category":"section"},{"location":"api/","page":"API reference","title":"API reference","text":"improper_explicit_imports","category":"page"},{"location":"api/#ExplicitImports.improper_explicit_imports","page":"API reference","title":"ExplicitImports.improper_explicit_imports","text":"improper_explicit_imports(mod::Module, file=pathof(mod); strict=true, skip=(Base => Core,))\n\nAttempts do detect various kinds of \"improper\" explicit imports taking place in mod and any submodules of mod.\n\nCurrently detects two classes of issues:\n\nnames which are explicitly imported but unused (stale)\nnames which are not public in mod\nhere, public means either exported or declared with the public keyword (requires Julia v1.11+)\none 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 and importing_from_submodule_owns_name for two variations on this.\n\nThe 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.\n\nThis 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).\n\nReturns 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 NamedTuples with the following keys:\n\nname::Symbol: the name being imported\nlocation::String: the location the access takes place\nvalue::Any: the which name points to in mod\nimporting_from::Module: the module the name is being imported from (e.g. in the example using Foo.X: bar, this would be X)\nwhichmodule::Module: the Base.which of the object\npublic_import::Bool: whether or not name is public or exported in importing_from. Checking if a name is marked public requires Julia v1.11+.\nimporting_from_owns_name::Bool: whether or not importing_from matches whichmodule and therefore is considered to directly \"own\" the name\nimporting_from_submodule_owns_name::Bool: whether or not whichmodule is a submodule of importing_from\nstale::Bool: whether or not the explicitly imported name is used\n\nIf strict=true, then returns nothing if mod could not be fully analyzed.\n\nIn non-breaking releases of ExplicitImports:\n\nmore columns may be added to these rows\nadditional rows may be returned which qualify as some other kind of \"improper\" access\n\nHowever, 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).\n\nSee 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.\n\n\n\n\n\n","category":"function"},{"location":"api/#Detecting-\"improper\"-access-of-names-from-other-modules","page":"API reference","title":"Detecting \"improper\" access of names from other modules","text":"","category":"section"},{"location":"api/","page":"API reference","title":"API reference","text":"improper_qualified_accesses","category":"page"},{"location":"api/#ExplicitImports.improper_qualified_accesses","page":"API reference","title":"ExplicitImports.improper_qualified_accesses","text":"improper_qualified_accesses(mod::Module, file=pathof(mod); skip=(Base => Core,))\n\nAttempts do detect various kinds of \"improper\" qualified accesses taking place in mod and any submodules of mod.\n\nCurrently, only detects cases in which the name is being accessed from a module mod for which:\n\nname is not exported from mod\nname is not declared public in mod (requires Julia v1.11+)\n\nThe 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.\n\nThis 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).\n\nReturns 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 NamedTuples with the following keys:\n\nname::Symbol: the name being accessed\nlocation::String: the location the access takes place\nvalue::Any: the which name points to in mod\naccessing_from::Module: the module the name is being accessed from (e.g. Module.name)\nwhichmodule::Module: the Base.which of the object\npublic_access::Bool: whether or not name is public or exported in accessing_from. Checking if a name is marked public requires Julia v1.11+.\naccessing_from_owns_name::Bool: whether or not accessing_from matches whichmodule and therefore is considered to directly \"own\" the name\naccessing_from_submodule_owns_name::Bool: whether or not whichmodule is a submodule of accessing_from\n\nIn non-breaking releases of ExplicitImports:\n\nmore columns may be added to these rows\nadditional rows may be returned which qualify as some other kind of \"improper\" access\n\nHowever, the result will be a Tables.jl-compatible row-oriented table (for each module), with at least all of the same columns.\n\nSee also print_explicit_imports to easily compute and print these results, improper_qualified_accesses_nonrecursive for a non-recursive version which ignores submodules, and check_all_qualified_accesses_via_owners for a version that throws errors, for regression testing.\n\nExample\n\njulia> using ExplicitImports\n\njulia> example_path = pkgdir(ExplicitImports, \"examples\", \"qualified.jl\");\n\njulia> print(read(example_path, String))\nmodule MyMod\nusing LinearAlgebra\n# sum is in `Base`, so we shouldn't access it from LinearAlgebra:\nn = LinearAlgebra.sum([1, 2, 3])\nend\n\njulia> include(example_path);\n\njulia> row = improper_qualified_accesses(MyMod, example_path)[1][2][1];\n\njulia> (; row.name, row.accessing_from, row.whichmodule)\n(name = :sum, accessing_from = LinearAlgebra, whichmodule = Base)\n\n\n\n\n\n","category":"function"},{"location":"api/#Checks-to-use-in-testing","page":"API reference","title":"Checks to use in testing","text":"","category":"section"},{"location":"api/","page":"API reference","title":"API reference","text":"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).","category":"page"},{"location":"api/","page":"API reference","title":"API reference","text":"The first such check is check_no_implicit_imports which aims to ensure there are no implicit exports used in the package.","category":"page"},{"location":"api/","page":"API reference","title":"API reference","text":"check_no_implicit_imports","category":"page"},{"location":"api/#ExplicitImports.check_no_implicit_imports","page":"API reference","title":"ExplicitImports.check_no_implicit_imports","text":"check_no_implicit_imports(mod::Module, file=pathof(mod); skip=(mod, Base, Core), ignore::Tuple=(),\n allow_unanalyzable::Tuple=())\n\nChecks that neither mod nor any of its submodules is relying on implicit imports, throwing an ImplicitImportsException if so, and returning nothing otherwise.\n\nThis function can be used in a package's tests, e.g.\n\n@test check_no_implicit_imports(MyPackage) === nothing\n\nAllowing some submodules to be unanalyzable\n\nPass 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.\n\nThese unanalyzable submodules can alternatively be included in ignore.\n\nAllowing some implicit imports\n\nThe 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:\n\n@test check_no_implicit_imports(MyPackage; skip=(Base, Core, DataFrames)) === nothing\n\nwould verify there are no implicit imports from modules other than Base, Core, and DataFrames.\n\nAdditionally, the keyword ignore can be passed to represent a tuple of items to ignore. These can be:\n\nmodules. Any submodule of mod matching an element of ignore is skipped. This can be used to allow the usage of implicit imports in some submodule of your package.\nsymbols: any implicit import of a name matching an element of ignore is ignored (does not throw)\nsymbol => module pairs. Any implicit import of a name matching that symbol from a module matching the module is ignored.\n\nOne can mix and match between these type of ignored elements. For example:\n\n@test check_no_implicit_imports(MyPackage; ignore=(:DataFrame => DataFrames, :ByRow, MySubModule)) === nothing\n\nThis would:\n\nIgnore any implicit import of DataFrame from DataFrames\nIgnore any implicit import of the name ByRow from any module.\nIgnore any implicit imports present in MyPackage's submodule MySubModule\n\nbut verify there are no other implicit imports.\n\n\n\n\n\n","category":"function"},{"location":"api/","page":"API reference","title":"API reference","text":"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.","category":"page"},{"location":"api/","page":"API reference","title":"API reference","text":"check_no_stale_explicit_imports\ncheck_all_explicit_imports_via_owners\ncheck_all_explicit_imports_are_public","category":"page"},{"location":"api/#ExplicitImports.check_no_stale_explicit_imports","page":"API reference","title":"ExplicitImports.check_no_stale_explicit_imports","text":"check_no_stale_explicit_imports(mod::Module, file=pathof(mod); ignore::Tuple=(), allow_unanalyzable::Tuple=())\n\nChecks that neither mod nor any of its submodules has stale (unused) explicit imports, throwing an StaleImportsException if so, and returning nothing otherwise.\n\nThis can be used in a package's tests, e.g.\n\n@test check_no_stale_explicit_imports(MyPackage) === nothing\n\nAllowing some submodules to be unanalyzable\n\nPass 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.\n\nAllowing some stale explicit imports\n\nIf ignore is supplied, it should be a tuple of Symbols, representing names that are allowed to be stale explicit imports. For example,\n\n@test check_no_stale_explicit_imports(MyPackage; ignore=(:DataFrame,)) === nothing\n\nwould check there were no stale explicit imports besides that of the name DataFrame.\n\n\n\n\n\n","category":"function"},{"location":"api/#ExplicitImports.check_all_explicit_imports_via_owners","page":"API reference","title":"ExplicitImports.check_all_explicit_imports_via_owners","text":"check_all_explicit_imports_via_owners(mod::Module, file=pathof(mod); ignore::Tuple=(),\n require_submodule_import=false,\n skip::Tuple{Vararg{Pair{Module, Module}, N}} where N=(Base => Core,)))\n\nChecks 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.\n\nThis can be used in a package's tests, e.g.\n\n@test check_all_explicit_imports_via_owners(MyPackage) === nothing\n\nAllowing some explicit imports via non-owner modules\n\nThe 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.\n\nFor example:\n\n@test check_all_explicit_imports_are_public(MyPackage; skip=(Base => Core, DataFrames => PrettyTables)) === nothing\n\nwould allow explicitly importing names which are owned by PrettyTables from DataFrames.\n\nIf ignore is supplied, it should be a tuple of Symbols, representing names that are allowed to be accessed from non-owner modules. For example,\n\n@test check_all_explicit_imports_via_owners(MyPackage; ignore=(:DataFrame,)) === nothing\n\nwould check there were no explicit imports from non-owner modules besides that of the name DataFrame.\n\nrequire_submodule_import\n\nIf 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.\n\nnon-fully-analyzable modules do not cause exceptions\n\nNote 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.\n\nSee also: improper_explicit_imports for programmatic access to such 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.\n\n\n\n\n\n","category":"function"},{"location":"api/#ExplicitImports.check_all_explicit_imports_are_public","page":"API reference","title":"ExplicitImports.check_all_explicit_imports_are_public","text":"check_all_explicit_imports_are_public(mod::Module, file=pathof(mod); ignore::Tuple=(),\n skip::Tuple{Vararg{Pair{Module, Module}, N}} where N=(Base => Core,))\n\nChecks 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.\n\nThis can be used in a package's tests, e.g.\n\n@test check_all_explicit_imports_are_public(MyPackage) === nothing\n\nAllowing some non-public explicit imports\n\nThe 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.\n\nFor example:\n\n@test check_all_explicit_imports_are_public(MyPackage; skip=(Base => Core, DataFrames => PrettyTables)) === nothing\n\nwould allow explicitly importing names which are public in PrettyTables from DataFrames.\n\nIf ignore is supplied, it should be a tuple of Symbols, representing names that are allowed to be imported from modules in which they are not public. For example,\n\n@test check_all_explicit_imports_are_public(MyPackage; ignore=(:DataFrame,)) === nothing\n\nwould check there were no non-public explicit imports besides that of the name DataFrame.\n\nnon-fully-analyzable modules do not cause exceptions\n\nNote 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.\n\nSee also: improper_explicit_imports for programmatic access to such 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.\n\n\n\n\n\n","category":"function"},{"location":"api/","page":"API reference","title":"API reference","text":"Lastly, we have one check related to detecting \"improper\" qualified accesses to names. 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.","category":"page"},{"location":"api/","page":"API reference","title":"API reference","text":"check_all_qualified_accesses_via_owners","category":"page"},{"location":"api/#ExplicitImports.check_all_qualified_accesses_via_owners","page":"API reference","title":"ExplicitImports.check_all_qualified_accesses_via_owners","text":"check_all_qualified_accesses_via_owners(mod::Module, file=pathof(mod); ignore::Tuple=(),\n require_submodule_access=false,\n skip::Tuple{Vararg{Pair{Module, Module}, N}} where N=(Base => Core,))\n\nChecks 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.\n\nThis can be used in a package's tests, e.g.\n\n@test check_all_qualified_accesses_via_owners(MyPackage) === nothing\n\nAllowing some qualified accesses via non-owner modules\n\nThe 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.\n\nFor example:\n\n@test check_all_qualified_accesses_via_owners(MyPackage; skip=(Base => Core, DataFrames => PrettyTables)) === nothing\n\nwould allow explicitly accessing names which are owned by PrettyTables from DataFrames.\n\nIf ignore is supplied, it should be a tuple of Symbols, representing names that are allowed to be accessed from non-owner modules. For example,\n\n@test check_all_qualified_accesses_via_owners(MyPackage; ignore=(:DataFrame,)) === nothing\n\nwould check there were no qualified accesses from non-owner modules besides that of the name DataFrame.\n\nIf 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.\n\nSee also: improper_qualified_accesses. Note that while that function may increase in scope and report other kinds of improper accesses, check_all_qualified_accesses_via_owners will not.\n\n\n\n\n\n","category":"function"},{"location":"api/#Usage-with-scripts-(such-as-runtests.jl)","page":"API reference","title":"Usage with scripts (such as runtests.jl)","text":"","category":"section"},{"location":"api/","page":"API reference","title":"API reference","text":"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.","category":"page"},{"location":"api/","page":"API reference","title":"API reference","text":"print_explicit_imports_script","category":"page"},{"location":"api/#ExplicitImports.print_explicit_imports_script","page":"API reference","title":"ExplicitImports.print_explicit_imports_script","text":"print_explicit_imports_script([io::IO=stdout,] path; skip=(Base, Core), warn_improper_explicit_imports=true)\n\nAnalyzes 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).\n\nNote that the particular printing may change in future non-breaking releases of ExplicitImports.\n\nwarning: Warning\n\n\nThe 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.\n\nKeyword arguments\n\nskip=(mod, Base, Core): any names coming from the listed modules (or any submodules thereof) will be skipped. Since mod is included by default, implicit imports of names exported from its own submodules will not count by default.\n\n\n\n\n\n","category":"function"},{"location":"api/#Non-recursive-variants","page":"API reference","title":"Non-recursive variants","text":"","category":"section"},{"location":"api/","page":"API reference","title":"API reference","text":"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.","category":"page"},{"location":"api/","page":"API reference","title":"API reference","text":"explicit_imports_nonrecursive\nimproper_qualified_accesses_nonrecursive\nimproper_explicit_imports_nonrecursive","category":"page"},{"location":"api/#ExplicitImports.explicit_imports_nonrecursive","page":"API reference","title":"ExplicitImports.explicit_imports_nonrecursive","text":"explicit_imports_nonrecursive(mod::Module, file=pathof(mod); skip=(mod, Base, Core), strict=true)\n\nA non-recursive version of explicit_imports, meaning it only analyzes the module mod itself, not any of its submodules; see that function for details.\n\nKeyword arguments\n\nskip=(mod, Base, Core): any names coming from the listed modules (or any submodules thereof) will be skipped. Since mod is included by default, implicit imports of names exported from its own submodules will not count by default.\nstrict=true: when strict=true, results will be nothing in the case that the analysis could not be performed accurately, due to e.g. dynamic include statements. When strict=false, results are returned in all cases, but may be inaccurate.\n\n\n\n\n\n","category":"function"},{"location":"api/#ExplicitImports.improper_qualified_accesses_nonrecursive","page":"API reference","title":"ExplicitImports.improper_qualified_accesses_nonrecursive","text":"improper_qualified_accesses_nonrecursive(mod::Module, file=pathof(mod); skip=(Base => Core,))\n\nA 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!).\n\nExample\n\njulia> using ExplicitImports\n\njulia> example_path = pkgdir(ExplicitImports, \"examples\", \"qualified.jl\");\n\njulia> print(read(example_path, String))\nmodule MyMod\nusing LinearAlgebra\n# sum is in `Base`, so we shouldn't access it from LinearAlgebra:\nn = LinearAlgebra.sum([1, 2, 3])\nend\n\njulia> include(example_path);\n\njulia> row = improper_qualified_accesses_nonrecursive(MyMod, example_path)[1];\n\njulia> (; row.name, row.accessing_from, row.whichmodule)\n(name = :sum, accessing_from = LinearAlgebra, whichmodule = Base)\n\n\n\n\n\n","category":"function"},{"location":"api/#ExplicitImports.improper_explicit_imports_nonrecursive","page":"API reference","title":"ExplicitImports.improper_explicit_imports_nonrecursive","text":"improper_explicit_imports_nonrecursive(mod::Module, file=pathof(mod); strict=true, skip=(Base => Core,))\n\nA 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!).\n\nIf strict=true, then returns nothing if mod could not be fully analyzed.\n\n\n\n\n\n","category":"function"},{"location":"","page":"Home","title":"Home","text":"CurrentModule = ExplicitImports","category":"page"},{"location":"","page":"Home","title":"Home","text":"using ExplicitImports, Markdown\ncontents = read(joinpath(pkgdir(ExplicitImports), \"README.md\"), String)\ncontents = replace(contents, \"[![stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://ericphanson.github.io/ExplicitImports.jl/stable/)\" => \"\")\nMarkdown.parse(contents)","category":"page"},{"location":"#Documentation-Index","page":"Home","title":"Documentation Index","text":"","category":"section"},{"location":"","page":"Home","title":"Home","text":"","category":"page"},{"location":"internals/#Internal-details","page":"Dev docs","title":"Internal details","text":"","category":"section"},{"location":"internals/#Implementation-strategy","page":"Dev docs","title":"Implementation strategy","text":"","category":"section"},{"location":"internals/","page":"Dev docs","title":"Dev docs","text":"[DONE hackily] Figure out what names used in the module are being used to refer to bindings in global scope (as opposed to e.g. shadowing globals).\nWe do this by parsing the code (thanks to JuliaSyntax), then reimplementing scoping rules on top of the parse tree\nThis is finicky, but assuming scoping doesn't change, should be robust enough (once the long tail of edge cases are dealt with...)\nCurrently, I don't handle the global keyword, so those may look like local variables and confuse things\nThis means we need access to the raw source code; pathof works well for packages, but for local modules one has to pass the path themselves. Also doesn't seem to work well for stdlibs in the sysimage\n[DONE] Figure out what implicit imports are available in the module, and which module they come from\ndone, via a magic ccall from Discourse, and Base.which.\n[DONE] Figure out which names have been explicitly imported already\nDone via parsing","category":"page"},{"location":"internals/","page":"Dev docs","title":"Dev docs","text":"Then we can put this information together to figure out what names are actually being used from other modules, and whose usage could be made explicit, and also which existing explicit imports are not being used.","category":"page"},{"location":"internals/#Internals","page":"Dev docs","title":"Internals","text":"","category":"section"},{"location":"internals/","page":"Dev docs","title":"Dev docs","text":"ExplicitImports.find_implicit_imports\nExplicitImports.get_names_used\nExplicitImports.analyze_all_names\nExplicitImports.inspect_session\nExplicitImports.FileAnalysis","category":"page"},{"location":"internals/#ExplicitImports.find_implicit_imports","page":"Dev docs","title":"ExplicitImports.find_implicit_imports","text":"find_implicit_imports(mod::Module; skip=(mod, Base, Core))\n\nGiven a module mod, returns a Dict{Symbol, @NamedTuple{source::Module,exporters::Vector{Module}}} showing names exist in mod's namespace which are available due to implicit exports by other modules. The dict's keys are those names, and the values are the source module that the name comes from, along with the modules which export the same binding that are available in mod due to implicit imports.\n\nIn the case of ambiguities (two modules exporting the same name), the name is unavailable in the module, and hence the name will not be present in the dict.\n\nThis is powered by Base.which.\n\n\n\n\n\n","category":"function"},{"location":"internals/#ExplicitImports.get_names_used","page":"Dev docs","title":"ExplicitImports.get_names_used","text":"get_names_used(file) -> FileAnalysis\n\nFigures out which global names are used in file, and what modules they are used within.\n\nTraverses static include statements.\n\nReturns a FileAnalysis object.\n\n\n\n\n\n","category":"function"},{"location":"internals/#ExplicitImports.analyze_all_names","page":"Dev docs","title":"ExplicitImports.analyze_all_names","text":"analyze_all_names(file)\n\nReturns a tuple of two items:\n\nper_usage_info: a table containing information about each name each time it was used\nuntainted_modules: a set containing modules found and analyzed successfully\n\n\n\n\n\n","category":"function"},{"location":"internals/#ExplicitImports.inspect_session","page":"Dev docs","title":"ExplicitImports.inspect_session","text":"ExplicitImports.inspect_session([io::IO=stdout,]; skip=(Base, Core), inner=print_explicit_imports)\n\nExperimental functionality to call inner (defaulting to print_explicit_imports) on each loaded package in the Julia session.\n\n\n\n\n\n","category":"function"},{"location":"internals/#ExplicitImports.FileAnalysis","page":"Dev docs","title":"ExplicitImports.FileAnalysis","text":"FileAnalysis\n\nContains structured analysis results.\n\nFields\n\nperusageinfo::Vector{PerUsageInfo}\nneeds_explicit_import::Set{@NamedTuple{name::Symbol,module_path::Vector{Symbol}, location::String}}\nunnecessary_explicit_import::Set{@NamedTuple{name::Symbol,module_path::Vector{Symbol}, location::String}}\nuntainted_modules::Set{Vector{Symbol}}: those which were analyzed and do not contain an unanalyzable include\n\n\n\n\n\n","category":"type"}] }