Setup.jl/src/autoloads.jl

271 lines
9.4 KiB
Julia

module Autoloads
using TOML
using REPL
export Bindings, AutoDot, PkgAutoloads, add_autoloads!
function __init__()
if isinteractive()
read_named_envs!()
repl_apply_autoloads!()
end
end
struct Bindings
macros::Vector{Symbol}
callables::Vector{Symbol}
variables::Vector{Symbol}
end
struct AutoDot end
Bindings(; macros::Vector{<:Union{Symbol, String}}=Symbol[],
callables::Vector{Symbol}=Symbol[], variables=Symbol[]) =
Bindings(Symbol.(macros), callables, variables)
struct PkgAutoloads
pkg::Union{Symbol, Expr}
exported::Bindings
unexported::Union{Bindings, AutoDot}
partial::Bool
end
PkgAutoloads(pkg::Union{Symbol, Expr};
exported::Bindings = Bindings(Symbol[], Symbol[], Symbol[]),
unexported::Union{Bindings, AutoDot} = AutoDot(),
partial::Bool = false) =
PkgAutoloads(pkg, exported, unexported, partial)
function PkgAutoloads(mod::Module)
hasparent(u::Union, m::Module) =
hasparent(u.a, m) && hasparent(u.b, m)
hasparent(x::Any, m::Module) =
parentmodule(x) === m
macros = Symbol[]
callables = Symbol[]
variables = Symbol[]
for symb in names(mod)
obj = getglobal(mod, symb)
if !hasparent(obj, mod) || obj === mod
elseif startswith(String(symb), '@')
push!(macros, symb)
elseif obj isa Function || obj isa Type
push!(callables, symb)
else
push!(variables, symb)
end
end
PkgAutoloads(nameof(mod),
exported = Bindings(
macros, callables, variables))
end
const AUTOLOADS =
(sources = Dict{Symbol, PkgAutoloads}(),
pkgs = Dict{Symbol, Expr}(),
exported = (
macros = Dict{Symbol, Symbol}(),
callables = Dict{Symbol, Symbol}(),
variables = Dict{Symbol, Symbol}()),
bindings = (
macros = Dict{Symbol, Set{Symbol}}(),
callables = Dict{Symbol, Set{Symbol}}(),
variables = Dict{Symbol, Set{Symbol}}()),
ondotaccess = Set{Symbol}(),
loaded = Set{Symbol}(),
partloaded = Dict{Symbol, Set{Symbol}}())
function add_autoloads!(autos::PkgAutoloads)
!haskey(AUTOLOADS.sources, autos.pkg) ||
error("Autoloads for $(autos.pkg) have already been registered")
pkgsym, pkg = if autos.pkg isa Symbol
autos.pkg, Expr(:., autos.pkg)
elseif autos.pkg isa Expr &&
Meta.isexpr(autos.pkg, :(.)) &&
length(autos.pkg.args) == 2 &&
autos.pkg.args[1] isa Symbol &&
autos.pkg.args[2] isa QuoteNode &&
autos.pkg.args[2].value isa Symbol
autos.pkg.args[2].value,
Expr(:., autos.pkg.args[1], autos.pkg.args[2].value)
end
AUTOLOADS.sources[pkgsym] = autos
AUTOLOADS.pkgs[pkgsym] = pkg
autos.partial && (AUTOLOADS.partloaded[pkgsym] = Set{Symbol}())
AUTOLOADS.exported.variables[pkgsym] = pkgsym
for category in (:macros, :callables, :variables)
autodot = autos.unexported isa AutoDot
bindings = autodot || Set(getfield(autos.unexported, category))
slots = getfield(AUTOLOADS.exported, category)
for binding in getfield(autos.exported, category)
if haskey(slots, binding)
@warn "An autload for $(String(category)[1:end-1]) $binding already exists"
else
slots[binding] = pkgsym
end
autodot || push!(bindings, binding)
end
if !autodot && !isempty(bindings)
getfield(AUTOLOADS.bindings, category)[pkgsym] = bindings
end
end
end
const NAMED_ENVS = Vector{Pair{String, Vector{Symbol}}}()
function read_named_envs!()
empty!(NAMED_ENVS)
for depot in Base.DEPOT_PATH
envdir = joinpath(depot, "environments")
isdir(envdir) || continue
for env in readdir(envdir)
if !isnothing(match(r"^__", env))
elseif !isnothing(match(r"^v\d+\.\d+$", env))
elseif isfile(joinpath(envdir, env, "Project.toml"))
proj = open(TOML.parse, joinpath(envdir, env, "Project.toml"))
pkgs = get(proj, "deps", Dict{String, Any}()) |>
keys |> collect |> sort .|> Symbol
push!(NAMED_ENVS, '@' * env => pkgs)
end
end
end
end
function find_named_env(pkg::Symbol)
for (env, pkgs) in NAMED_ENVS
if pkg in pkgs
return env
end
end
end
function autoload_loadpkg(name::Symbol, thing::Union{Symbol, Nothing}=nothing)
if name AUTOLOADS.loaded && haskey(AUTOLOADS.pkgs, name)
AUTOLOADS.sources[name].partial && thing in AUTOLOADS.partloaded[name] &&
return
pkg = AUTOLOADS.pkgs[name]
if length(pkg.args) == 1 && isnothing(Base.find_package(String(first(pkg.args))))
named_env = find_named_env(name)
if !isnothing(named_env)
autoload_loadpkg_namedenv(name, named_env, thing)
else
@warn "Cannot autoload $name, as it is not availible in the current environment stack"
end
return
end
try
if !isnothing(thing) && AUTOLOADS.sources[name].partial
Core.eval(Main, Expr(:import, Expr(:., pkg.args..., thing)))
push!(AUTOLOADS.partloaded[name], thing)
else
Core.eval(Main, Expr(:using, pkg))
push!(AUTOLOADS.loaded, name)
end
nothing
catch err
@warn "Failed to automatically load $name" err
end
end
end
function autoload_loadpkg_namedenv(name::Symbol, env::String, thing::Union{Symbol, Nothing}=nothing)
pkg = AUTOLOADS.pkgs[name]
if Base.active_project() == Base.load_path_expand("@v#.#")
pushfirst!(LOAD_PATH, env)
try
if !isnothing(thing) && AUTOLOADS.sources[name].partial
Core.eval(Main, Expr(:import, Expr(:., pkg.args..., thing)))
push!(AUTOLOADS.partloaded[name], thing)
else
Core.eval(Main, Expr(:using, pkg))
push!(AUTOLOADS.loaded, name)
end
finally
popfirst!(LOAD_PATH)
end
else
@warn "Cannot autoload $name, but it is availible in the $env environment"
end
end
function autoload_scan!(ast::Expr, localbindings::Vector{Symbol}=Symbol[])
if Meta.isexpr(ast, :macrocall)
pkg = if first(ast.args) isa Symbol && first(ast.args) localbindings
get(AUTOLOADS.exported.macros, first(ast.args), nothing)
elseif Meta.isexpr(first(ast.args), :(.)) &&
length(first(ast.args).args) == 2 &&
(root = first(ast.args).args[1]) isa Symbol &&
first(ast.args).args[2] isa QuoteNode &&
(child = first(ast.args).args[2].value) isa Symbol &&
haskey(AUTOLOADS.bindings.macros, root) &&
haskey(AUTOLOADS.bindings.macros[root], child)
AUTOLOADS.bindings.macros[root]
end
if !isnothing(pkg) autoload_loadpkg(pkg, first(ast.args)) end
elseif Meta.isexpr(ast, :call)
pkg = if first(ast.args) isa Symbol && first(ast.args) localbindings
get(AUTOLOADS.exported.callables, first(ast.args), nothing)
elseif Meta.isexpr(first(ast.args), :(.)) &&
(root = first(ast.args).args[1]) isa Symbol &&
root AUTOLOADS.ondotaccess
root
elseif Meta.isexpr(first(ast.args), :(.)) &&
length(first(ast.args).args) == 2 &&
(root = first(ast.args).args[1]) isa Symbol &&
first(ast.args).args[2] isa QuoteNode &&
(child = first(ast.args).args[2].value) isa Symbol &&
haskey(AUTOLOADS.bindings.callables, root) &&
child AUTOLOADS.bindings.callables[root]
root
end
isnothing(pkg) || autoload_loadpkg(pkg, first(ast.args))
elseif Meta.isexpr(ast, :(.)) &&
length(ast.args) == 2 &&
(root = ast.args[1]) isa Symbol &&
ast.args[2] isa QuoteNode &&
(child = ast.args[2].value) isa Symbol &&
(root AUTOLOADS.ondotaccess ||
(haskey(AUTOLOADS.bindings.variables, root) &&
child AUTOLOADS.bindings.variables[root]) ||
(haskey(AUTOLOADS.bindings.callables, root) &&
child AUTOLOADS.bindings.callables[root]))
autoload_loadpkg(root)
elseif ast.head (:using, :import, :quote)
return
elseif Meta.isexpr(ast, :(=)) && first(ast.args) isa Symbol
push!(localbindings, first(ast.args))
end
for arg in ast.args
autoload_scan!(arg, localbindings)
end
end
function autoload_scan!(var::Symbol, localbindings::Vector{Symbol}=Symbol[])
if var localbindings && var names(Main)
pkg = @something(get(AUTOLOADS.exported.variables, var, nothing),
get(AUTOLOADS.exported.callables, var, nothing),
Some(nothing))
if !isnothing(pkg) autoload_loadpkg(pkg, var) end
end
end
autoload_scan!(::Any, ::Vector{Symbol}=Symbol[]) = nothing
function repl_scan_autoloads!(ast::Expr)
@static if VERSION < v"1.9"
autoload_scan!(ast)
else
if Base.active_module() == Main
autoload_scan!(ast)
end
end
ast
end
repl_scan_autoloads!(val::Any) = val
repl_apply_autoloads!() =
pushfirst!(REPL.repl_ast_transforms, repl_scan_autoloads!)
end