[ANN] ExplicitImports.jl: tooling to help use and maintain explicit imports in your package

Background: what are implicit and explicit imports?

An explicit import is a statement like using SomePkg: foo or import SomePkg: foo which explicitly brings the name foo into your module (often another package). In contrast, an implicit one is a statement like using SomePkg, which brings all names exported by SomePkg into your module.

Implicit imports can be convenient, but they occasionally have issues in which another package OtherPkg starts exporting the same name foo (pointing to a different function, say), which makes it ambiguous which function should be associated to the name foo in your module.

Some also criticize that with implicit imports, if you only have access to the source code, you can’t see where each name is coming from. (If you are in a REPL, use @which to easily do so).

Explicit imports, on the other hand, can be quite inconvenient. You need to manually specify each name that you want to import. You also need to maintain this list as you use more names or stop using some names.

The choice of implicit vs explicit exports can be somewhat contentious, and I think in general the right choice depends on the specific usage (and your preference). I would prefer if folks don’t use this thread to debate which to use, and focus on the functionality ExplicitImports.jl provides (or fails to provide, as the case may be).

What does ExplicitImports.jl do?

ExplicitImports.jl attempts to make it easier to use and maintain lists of explicit imports. The main interactive entrypoint is:

using ExplicitImports, MyPackage
print_explicit_imports(MyPackage)

For example:

julia> using ExplicitImports

julia> print_explicit_imports(ExplicitImports)
Module ExplicitImports is relying on implicit imports for 6 names. These could be explicitly imported as follows:

```julia
using AbstractTrees: AbstractTrees
using AbstractTrees: Leaves
using AbstractTrees: TreeCursor
using AbstractTrees: children
using AbstractTrees: nodevalue
using JuliaSyntax: JuliaSyntax
```

Additionally, ExplicitImports has stale explicit imports for these unused names:
- GreenNode

This allows one to develop some code using implicit imports, then call print_explicit_imports to get a list of explicit import statements to copy into their code.

Additionally, if one would like to only rely on explicit imports, you can add ExplicitImports as a test dependency, and add check_no_implicit_imports(ExplicitImports) to your tests. ExplicitImports is registered in General to make it convenient to add as a test dependency.

There’s a bit more discussion and a few more features in the docs, so please check those out for more!

And if you run into any bugs or problems, please file an issue. ExplicitImports works by parsing your code and trying to re-implement Julia’s scoping rules to figure out whether any given name refers to a global that may be implicitly imported, or just a local variable. There’s a ton of edge cases and it’s very possible there are common failure modes. I am hoping we can squash those as they arise, and rely on the ignore kwarg in check_no_implicit_imports in the meantime to manually suppress false positives. (And if you know about parsing and have ideas about how I can do it better, please file an issue or PR!).

50 Likes

This would be a nice addition to the language server code actions. The language server should already know where bindings come from so you wouldn’t have to traverse the source code in this case (I think).

5 Likes

Awesome idea!!

Is the plan to (eventually) add this to Aqua.jl?

3 Likes

I think LanguageServer integration would be great. I have no idea how that works though.

I am not sure if Aqua would want it, since it adds two dependencies (AbstractTrees and JuliaSyntax - though we could use Base.JuliaSyntax to get rid of the latter). It also might be too immature at this phase. Lastly I think automatically erroring on stuff like implicit imports seems to opinionated/aggressive to me. But I think it’s really up to the Aqua maintainers in terms of what they want to do and what the vision for the package is.

1 Like

If you, or anyone else, wants to give it a try perhaps the PR where I implemented import sorting could be useful to get an idea about what you need to add.

1 Like

Thanks for the nice package! Right now, it requires Julia v1.10. Thus, we cannot use it for packages running CI also with older versions of Julia. Do you have a suggestion for a workaround for such a case?

Would just putting a conditional on the test set work:

if VERSION >= v"1.10"
    @testset ...
end

Not in a nice way, since we need to add ExplicitImports.jl to test/Project.toml and the package cannot be installed on older Julia versions.
The only way I see would be to depend on Pkg for tests and install ExplicitImports.jl after a check for the Julia version, but that appears to be more cumbersome than I would like.

I think we should be able to drop the julia compat bound to at least 1.7 (I’m using the destructuring syntax there so dropping it further would require not using that syntax). But when I tried some tests failed, so I need to debug further.

That would be very nice, thanks a lot.

1 Like

Just to say v1.0.1 is out and supports Julia 1.7+.

6 Likes

Nice, thanks!

ExplicitImports v1.44 is out, which fixes a lot of bugs in determining whether or not a name comes from an external package or is local to the module. I am hopeful that eventually JuliaLowering.jl will provide better access to scope information than my hacky parsing, and at some point we can switch to using that instead for precise information. However, in the meantime I think what we have is “good enough” for a lot of practical use, and it’s easy enough to whackamole false positives as they arise (please do file issues!).

Also, since ExplicitImports v1.4, the printing is grouped by package and linewidth aware (with a default of 80):

julia> print_explicit_imports(ExplicitImports; linewidth=60)
Module ExplicitImports is relying on implicit imports for 7 names. These could be explicitly imported as follows:

```julia
using AbstractTrees: AbstractTrees, Leaves, TreeCursor,
                     children, nodevalue
using JuliaSyntax: JuliaSyntax, @K_str
```

I think this formatting is a bit nicer than the long list of using Foo: bar’s.

13 Likes

ExplicitImports v1.5 and v1.6 add a bunch of functionality to make it easier to work with qualified names and explicit imports. It is all summarized here:

Problem Example Interactive detection Programmatic detection Regression-testing check
Implicit imports using LinearAlgebra print_explicit_imports implicit_imports check_no_implicit_imports
Non-owning import using LinearAlgebra: map print_explicit_imports improper_explicit_imports check_all_explicit_imports_via_owners
Non-public import using LinearAlgebra: _svd! print_explicit_imports with report_non_public=true improper_explicit_imports check_all_explicit_imports_are_public
Stale import using LinearAlgebra: svd # unused print_explicit_imports improper_explicit_imports check_no_stale_explicit_imports
Non-owning access LinearAlgebra.map print_explicit_imports improper_qualified_accesses check_all_qualified_accesses_via_owners
Non-public access LinearAlgebra._svd! print_explicit_imports with report_non_public=true improper_qualified_accesses check_all_qualified_accesses_are_public
Self-qualified access Foo.bar within the module Foo print_explicit_imports improper_qualified_accesses check_no_self_qualified_accesses

(from https://github.com/ericphanson/ExplicitImports.jl/tree/main?tab=readme-ov-file#summary)

5 Likes

Actually, I preferred the previous way,

I like having each using in its own line.

Any chance this can be controlled by a setting (or a keyword argument to print_explicit_imports) ?

Sure, I just tagged ExplicitExports v1.7 which adds a separate_lines kwarg to print_explicit_imports, and also unrelatedly allows the package to work on Julia 1.6 instead of only 1.7+.

4 Likes