Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
7d6c264
wip: upgrade to JuliaSyntax v1
ericphanson Jun 4, 2025
c14d8b4
gitignore for ]dev --local
ericphanson Jun 12, 2025
3ec279d
undo simplify hashing hack
ericphanson Jun 12, 2025
e0c4f8d
pkg roundtrip
ericphanson Jun 12, 2025
27fcf8d
improve debugging
ericphanson Jun 12, 2025
b6a5e4e
change for quoting in qualified names (JS#324)
ericphanson Jun 12, 2025
b3871aa
wip: hashing fix
ericphanson Jun 12, 2025
3303b8c
switch to object identity
ericphanson Jun 12, 2025
0107019
fix for and generator iteration
ericphanson Jun 12, 2025
a3afdec
stricter
ericphanson Jun 12, 2025
73c8b4b
inline function defs do not have K"=" anymore
ericphanson Jun 12, 2025
43f6b77
fix do-blocks
ericphanson Jun 12, 2025
1099a73
bump version
ericphanson Jun 12, 2025
9bb164a
support new error on 1.12
ericphanson Jun 12, 2025
8cecc34
wip
ericphanson Jun 13, 2025
7f21afd
support module aliases
ericphanson Jun 13, 2025
ed96729
format tests & wrap in testset
ericphanson Jun 13, 2025
6c6e52f
Merge remote-tracking branch 'origin/eph/tests' into eph/js_v1
ericphanson Jun 13, 2025
ebdf549
re-enable aqua
ericphanson Jun 13, 2025
ec01aa4
Merge remote-tracking branch 'origin/eph/tests' into eph/js_v1
ericphanson Jun 13, 2025
42eec55
support 1.12-beta
ericphanson Jun 13, 2025
89563e4
Merge remote-tracking branch 'origin/eph/fix-main' into eph/tests
ericphanson Jun 13, 2025
156f680
Merge branch 'eph/tests' into eph/js_v1
ericphanson Jun 13, 2025
117bfa1
Merge remote-tracking branch 'origin/main' into eph/tests
ericphanson Jun 13, 2025
b8295d0
Merge branch 'eph/tests' into eph/js_v1
ericphanson Jun 13, 2025
326f419
format
ericphanson Jun 13, 2025
7a17522
Merge branch 'eph/tests' into eph/js_v1
ericphanson Jun 13, 2025
3d164d7
Merge branch 'main' into eph/js_v1
ericphanson Jun 13, 2025
9ac35b0
Merge branch 'eph/js_v1' into eph/julia_lowering
ericphanson Jun 13, 2025
eef8c12
Merge remote-tracking branch 'origin/main' into eph/julia_lowering
ericphanson Jul 21, 2025
9a81195
set up code to vendor JuliaLowering
ericphanson Jul 21, 2025
6ca7cc9
commit JuliaLowering
ericphanson Jul 21, 2025
461ee16
rm sources
ericphanson Jul 21, 2025
5e0682e
wip
ericphanson Jul 21, 2025
b7b5e9b
use `JuliaLowering.SyntaxTree`
ericphanson Jul 21, 2025
147d58a
do some lowering
ericphanson Jul 21, 2025
30f9fab
add issue 120
ericphanson Jul 21, 2025
92790fd
wip
ericphanson Jul 22, 2025
a48b1ad
wip
ericphanson Jul 22, 2025
b8f35de
wip
ericphanson Jul 30, 2025
23ad1b0
wip
ericphanson Jul 30, 2025
7e73b5e
wip
ericphanson Jul 30, 2025
16e9fd7
wip
ericphanson Jul 31, 2025
fa1f7d6
update
ericphanson Jul 31, 2025
41d072a
wip
ericphanson Aug 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "ExplicitImports"
uuid = "7d51a73a-1435-4ff3-83d9-f097790105c7"
authors = ["Eric P. Hanson"]
version = "1.13.1"
authors = ["Eric P. Hanson"]

[deps]
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
Expand Down
29 changes: 19 additions & 10 deletions src/ExplicitImports.jl
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
module ExplicitImports

MUST_USE_JULIA_LOWERING::Bool = true

#! explicit-imports: off
# We vendor some dependencies to avoid compatibility problems. We tell ExplicitImports to ignore
# these as we don't want it to recurse into vendored dependencies.
# We also add `Vendored` to `ignore_submodules` elsewhere.
module Vendored
include(joinpath("vendored", "JuliaSyntax", "src", "JuliaSyntax.jl"))
include(joinpath("vendored", "AbstractTrees", "src", "AbstractTrees.jl"))
include(joinpath("vendored", "JuliaLowering", "src", "JuliaLowering.jl"))
end
#! explicit-imports: on

using .Vendored.JuliaLowering

using .Vendored.JuliaSyntax
# suppress warning about Base.parse collision, even though parse is never used
# this avoids a warning when loading the package while creating an unused explicit import
Expand All @@ -23,8 +28,11 @@ using Markdown: Markdown
using PrecompileTools: @setup_workload, @compile_workload
using Pkg: Pkg

# debug
parsefile

# we'll borrow their `@_public` macro; if this goes away, we can get our own
JuliaSyntax.@_public ignore_submodules
JuliaSyntax.@_public public ignore_submodules

export print_explicit_imports, explicit_imports, check_no_implicit_imports,
explicit_imports_nonrecursive
Expand Down Expand Up @@ -60,6 +68,7 @@ const STRICT_PRINTING_KWARG = """
const STRICT_NONRECURSIVE_KWARG = """
* `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."""

include("lower.jl")
include("parse_utilities.jl")
include("find_implicit_imports.jl")
include("get_names_used.jl")
Expand Down Expand Up @@ -139,7 +148,7 @@ function explicit_imports(mod::Module, file=pathof(mod); skip=(mod, Base, Core),
end

submodules = find_submodules(mod, file)
fill_cache!(file_analysis, last.(submodules))
fill_cache!(file_analysis, last.(submodules), mod)
return [submodule => explicit_imports_nonrecursive(submodule, path; skip, warn_stale,
file_analysis=file_analysis[path],
strict)
Expand Down Expand Up @@ -206,7 +215,7 @@ function explicit_imports_nonrecursive(mod::Module, file=pathof(mod);
# deprecated
warn_stale=nothing,
# private undocumented kwarg for hoisting this analysis
file_analysis=get_names_used(file))
file_analysis=get_names_used(file, mod))
check_file(file)
if warn_stale !== nothing
@warn "[explicit_imports_nonrecursive] keyword argument `warn_stale` is deprecated and does nothing" _id = :explicit_imports_explicit_imports_warn_stale maxlog = 1
Expand Down Expand Up @@ -402,10 +411,10 @@ function find_submodule_path(file, submodule)
return path
end

function fill_cache!(file_analysis::Dict, files)
function fill_cache!(file_analysis::Dict, files, mod)
for _file in files
if !haskey(file_analysis, _file)
file_analysis[_file] = get_names_used(_file)
file_analysis[_file] = get_names_used(_file, mod)
end
end
return file_analysis
Expand All @@ -430,10 +439,10 @@ end

include("precompile.jl")

@setup_workload begin
@compile_workload begin
sprint(print_explicit_imports, ExplicitImports, @__FILE__; context=:color => true)
end
end
# @setup_workload begin
# @compile_workload begin
# sprint(print_explicit_imports, ExplicitImports, @__FILE__; context=:color => true)
# end
# end

end
4 changes: 2 additions & 2 deletions src/deprecated.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ function stale_explicit_imports(mod::Module, file=pathof(mod); strict=true)
@warn "[stale_explicit_imports] deprecated in favor of `improper_explicit_imports`" _id = :explicit_imports_stale_explicit_imports maxlog = 1
submodules = find_submodules(mod, file)
file_analysis = Dict{String,FileAnalysis}()
fill_cache!(file_analysis, last.(submodules))
fill_cache!(file_analysis, last.(submodules), mod)
return [submodule => stale_explicit_imports_nonrecursive(submodule, path;
file_analysis=file_analysis[path],
strict)
Expand All @@ -13,7 +13,7 @@ end
function stale_explicit_imports_nonrecursive(mod::Module, file=pathof(mod);
strict=true,
# private undocumented kwarg for hoisting this analysis
file_analysis=get_names_used(file))
file_analysis=get_names_used(file, mod))
check_file(file)
@warn "[stale_explicit_imports_nonrecursive] deprecated in favor of `improper_explicit_imports_nonrecursive`" _id = :explicit_imports_stale_explicit_imports maxlog = 1

Expand Down
35 changes: 19 additions & 16 deletions src/get_names_used.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Base.@kwdef struct PerUsageInfo
function_arg::Bool
is_assignment::Bool
module_path::Vector{Symbol}
scope_path::Vector{JuliaSyntax.SyntaxNode}
scope_path::Vector{JuliaLowering.SyntaxTree}
struct_field_or_type_param::Bool
for_loop_index::Bool
generator_index::Bool
Expand Down Expand Up @@ -345,31 +345,33 @@ function is_double_colon_LHS(leaf)
return child_index(leaf) == 1
end

DEBUG = []
# Here we use the magic of AbstractTrees' `TreeCursor` so we can start at
# a leaf and follow the parents up to see what scopes our leaf is in.
# TODO-someday- cleanup. This basically has two jobs: check is function arg etc, and figure out the scope/module path.
# We could do these two things separately for more clarity.
function analyze_name(leaf; debug=false)
function analyze_name(leaf)
# Ok, we have a "name". Let us work our way up and try to figure out if it is in local scope or not
function_arg = is_function_definition_arg(leaf)
struct_field_or_type_param = is_struct_type_param(leaf) || is_struct_field_name(leaf)
for_loop_index = is_for_arg(leaf)
generator_index = is_generator_arg(leaf)
catch_arg = is_catch_arg(leaf)
module_path = Symbol[]
scope_path = JuliaSyntax.SyntaxNode[]
scope_path = JuliaLowering.SyntaxTree[]
is_assignment = false
is_global = false
node = leaf
idx = 1

# @show try_get_val(nodevalue(leaf))
# @show kind(nodevalue(get_parent(leaf, 1)))
push!(DEBUG, leaf)
prev_node = nothing
while true
# update our state
val = get_val(node)
k = kind(node)
args = nodevalue(node).node.raw.children
args = children(nodevalue(node).node)

debug && println(val, ": ", k)
# Constructs that start a new local scope. Note `let` & `macro` *arguments* are not explicitly supported/tested yet,
# but we can at least keep track of scope properly.
if k in
Expand All @@ -389,7 +391,7 @@ function analyze_name(leaf; debug=false)
return kind(arg.node) == K"Identifier"
end
if !isempty(ids)
push!(module_path, first(ids).node.val)
push!(module_path, get_val(first(ids).node))
end
push!(scope_path, nodevalue(node).node)
end
Expand Down Expand Up @@ -417,17 +419,17 @@ function analyze_name(leaf; debug=false)
end

"""
analyze_all_names(file)
analyze_all_names(file, in_mod::Module)

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
"""
function analyze_all_names(file)
function analyze_all_names(file, in_mod::Module)
# we don't use `try_parse_wrapper` here, since there's no recovery possible
# (no other files we know about to look at)
tree = SyntaxNodeWrapper(file)
tree = SyntaxNodeWrapper(file, in_mod)
# in local scope, a name refers to a global if it is read from before it is assigned to, OR if the global keyword is used
# a name refers to a local otherwise
# so we need to traverse the tree, keeping track of state like: which scope are we in, and for each name, in each scope, has it been used
Expand All @@ -444,7 +446,7 @@ function analyze_all_names(file)
function_arg::Bool,
is_assignment::Bool,
module_path::Vector{Symbol},
scope_path::Vector{JuliaSyntax.SyntaxNode},
scope_path::Vector{JuliaLowering.SyntaxTree},
struct_field_or_type_param::Bool,
for_loop_index::Bool,
generator_index::Bool,
Expand Down Expand Up @@ -495,6 +497,7 @@ function analyze_all_names(file)
end
ret = analyze_name(leaf)
push!(seen_modules, ret.module_path)
# @show name, qualified_by, import_type, explicitly_imported_by, location, ret
push!(per_usage_info,
(; name, qualified_by, import_type, explicitly_imported_by, location, ret...))
end
Expand Down Expand Up @@ -532,7 +535,7 @@ end
# in JuliaSyntax 1.0. This is very slow and also not quite the semantics we want anyway.
# Here, we wrap our nodes in a custom type that only compares object identity.
struct SyntaxNodeList
nodes::Vector{JuliaSyntax.SyntaxNode}
nodes::Vector{JuliaLowering.SyntaxTree}
end

function Base.:(==)(a::SyntaxNodeList, b::SyntaxNodeList)
Expand Down Expand Up @@ -648,18 +651,18 @@ function setdiff_no_metadata(set1, set2)
end

"""
get_names_used(file) -> FileAnalysis
get_names_used(file, in_mod::Module) -> 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.
"""
function get_names_used(file)
function get_names_used(file, in_mod::Module)
check_file(file)
# Here we get 1 row per name per usage
per_usage_info, untainted_modules = analyze_all_names(file)
per_usage_info, untainted_modules = analyze_all_names(file, in_mod)

names_used_for_global_bindings = get_global_names(per_usage_info)
explicit_imports = get_explicit_imports(per_usage_info)
Expand Down
6 changes: 3 additions & 3 deletions src/improper_explicit_imports.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
function analyze_explicitly_imported_names(mod::Module, file=pathof(mod);
# private undocumented kwarg for hoisting this analysis
file_analysis=get_names_used(file))
file_analysis=get_names_used(file, mod))
check_file(file)
(; per_usage_info, unnecessary_explicit_import, tainted) = filter_to_module(file_analysis,
mod)
Expand Down Expand Up @@ -174,7 +174,7 @@ function improper_explicit_imports_nonrecursive(mod::Module, file=pathof(mod);
strict=true,
allow_internal_imports=true,
# private undocumented kwarg for hoisting this analysis
file_analysis=get_names_used(file))
file_analysis=get_names_used(file, mod))
check_file(file)
problematic, tainted = analyze_explicitly_imported_names(mod, file; file_analysis)

Expand Down Expand Up @@ -255,7 +255,7 @@ function improper_explicit_imports(mod::Module, file=pathof(mod); strict=true,
check_file(file)
submodules = find_submodules(mod, file)
file_analysis = Dict{String,FileAnalysis}()
fill_cache!(file_analysis, last.(submodules))
fill_cache!(file_analysis, last.(submodules), mod)
return [submodule => improper_explicit_imports_nonrecursive(submodule, path; strict,
file_analysis=file_analysis[path],
skip,
Expand Down
6 changes: 3 additions & 3 deletions src/improper_qualified_accesses.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

function analyze_qualified_names(mod::Module, file=pathof(mod);
# private undocumented kwarg for hoisting this analysis
file_analysis=get_names_used(file))
file_analysis=get_names_used(file, mod))
check_file(file)
(; per_usage_info, tainted) = filter_to_module(file_analysis, mod)
# Do we want to do anything with `tainted`? This means there is unanalyzable code here
Expand Down Expand Up @@ -128,7 +128,7 @@ function improper_qualified_accesses_nonrecursive(mod::Module, file=pathof(mod);
# deprecated, does nothing
require_submodule_access=nothing,
# private undocumented kwarg for hoisting this analysis
file_analysis=get_names_used(file))
file_analysis=get_names_used(file, mod))
check_file(file)
if require_submodule_access !== nothing
@warn "[improper_qualified_accesses_nonrecursive] `require_submodule_access` is deprecated and unused" _id = :explicit_imports_improper_qualified_accesses_require_submodule_access maxlog = 1
Expand Down Expand Up @@ -229,7 +229,7 @@ function improper_qualified_accesses(mod::Module, file=pathof(mod);
end
submodules = find_submodules(mod, file)
file_analysis = Dict{String,FileAnalysis}()
fill_cache!(file_analysis, last.(submodules))
fill_cache!(file_analysis, last.(submodules), mod)
return [submodule => improper_qualified_accesses_nonrecursive(submodule, path;
file_analysis=file_analysis[path],
skip,
Expand Down
Loading
Loading