Skip to content

Commit 37ba52c

Browse files
authored
Merge pull request #402 from gcv/fix-relative-imports
Fix relative import resolution
2 parents ce98ea3 + e3e4219 commit 37ba52c

File tree

5 files changed

+175
-10
lines changed

5 files changed

+175
-10
lines changed

src/StaticLint.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@ function (state::ResolveOnly)(x::EXPR)
143143
else
144144
s0 = state.scope
145145
end
146+
147+
# NEW: late import resolution (idempotent for already-resolved imports)
148+
resolve_import(x, state)
149+
146150
resolve_ref(x, state)
147151

148152
traverse(x, state)

src/imports.jl

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,27 @@ function resolve_import_block(x::EXPR, state::State, root, usinged, markfinal=tr
2222
elseif root isa Scope && parentof(root) !== nothing
2323
root = parentof(root)
2424
else
25+
# Too many dots
26+
seterror!(arg, RelativeImportTooManyDots)
2527
return
2628
end
2729
elseif isidentifier(arg) || (i == n && (CSTParser.ismacroname(arg) || isoperator(arg)))
28-
root = maybe_lookup(hasref(arg) ? refof(arg) : _get_field(root, arg, state), state)
30+
cand = hasref(arg) ? refof(arg) : _get_field(root, arg, state)
31+
if cand === nothing
32+
# Cannot resolve now (e.g. sibling not yet defined). Schedule a retry.
33+
if state isa Toplevel
34+
# the import/using expression
35+
imp = StaticLint.get_parent_fexpr(arg, y -> headof(y) === :using || headof(y) === :import)
36+
#imp !== nothing && push!(state.resolveonly, imp)
37+
imp !== nothing && (imp state.resolveonly || push!(state.resolveonly, imp))
38+
# the enclosing module (so we re-resolve refs within it)
39+
mod = StaticLint.maybe_get_parent_fexpr(imp, CSTParser.defines_module)
40+
#mod !== nothing && push!(state.resolveonly, mod)
41+
mod !== nothing && (mod state.resolveonly || push!(state.resolveonly, mod))
42+
end
43+
return
44+
end
45+
root = maybe_lookup(cand, state)
2946
setref!(arg, root)
3047
if i == n
3148
markfinal && _mark_import_arg(arg, root, state, usinged)
@@ -38,12 +55,21 @@ function resolve_import_block(x::EXPR, state::State, root, usinged, markfinal=tr
3855
end
3956

4057
function resolve_import(x::EXPR, state::State, root=getsymbols(state))
41-
if headof(x) === :using || headof(x) === :import
42-
usinged = headof(x) === :using
58+
if (headof(x) === :using || headof(x) === :import)
59+
usinged = (headof(x) === :using)
4360
if length(x.args) > 0 && isoperator(headof(x.args[1])) && valof(headof(x.args[1])) == ":"
44-
root = resolve_import_block(x.args[1].args[1], state, root, false, false)
61+
root2 = resolve_import_block(x.args[1].args[1], state, root, false, false)
62+
if root2 === nothing
63+
# schedule a retry like above
64+
if state isa Toplevel
65+
push!(state.resolveonly, x)
66+
mod = StaticLint.maybe_get_parent_fexpr(x, CSTParser.defines_module)
67+
mod !== nothing && push!(state.resolveonly, mod)
68+
end
69+
return
70+
end
4571
for i = 2:length(x.args[1].args)
46-
resolve_import_block(x.args[1].args[i], state, root, usinged)
72+
resolve_import_block(x.args[1].args[i], state, root2, usinged)
4773
end
4874
else
4975
for i = 1:length(x.args)
@@ -80,6 +106,9 @@ function _mark_import_arg(arg, par, state, usinged)
80106
elseif par isa Binding && par.val isa Binding && par.val.val isa EXPR && CSTParser.defines_module(par.val.val)
81107
add_to_imported_modules(state.scope, Symbol(valofid(arg)), scopeof(par.val.val))
82108
end
109+
else
110+
# import binds the name in the current scope
111+
state.scope.names[valofid(arg)] = bindingof(arg)
83112
end
84113
end
85114
end
@@ -97,7 +126,7 @@ function add_to_imported_modules(scope::Scope, name::Symbol, val)
97126
if scope.modules isa Dict
98127
scope.modules[name] = val
99128
else
100-
Dict(name => val)
129+
scope.modules = Dict{Symbol,Any}(name => val)
101130
end
102131
end
103132
no_modules_above(s::Scope) = !CSTParser.defines_module(s.expr) || s.parent === nothing || no_modules_above(s.parent)

src/linting/checks.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
IndexFromLength,
3434
FileTooBig,
3535
FileNotAvailable,
36+
RelativeImportTooManyDots,
3637
)
3738

3839
const LintCodeDescriptions = Dict{LintCodes,String}(
@@ -66,7 +67,8 @@ const LintCodeDescriptions = Dict{LintCodes,String}(
6667
IncludePathContainsNULL => "Cannot include file, path contains NULL characters.",
6768
IndexFromLength => "Indexing with indices obtained from `length`, `size` etc is discouraged. Use `eachindex` or `axes` instead.",
6869
FileTooBig => "File too big, not following include.",
69-
FileNotAvailable => "File not available."
70+
FileNotAvailable => "File not available.",
71+
RelativeImportTooManyDots => "Relative import has more leading dots than available module nesting.",
7072
)
7173

7274
haserror(m::Meta) = m.error !== nothing

src/references.jl

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,16 +115,29 @@ end
115115

116116
function resolve_ref_from_module(x::EXPR, scope::Scope, state::State)::Bool
117117
hasref(x) && return true
118-
resolved = false
119118

120119
mn = nameof_expr_to_resolve(x)
121120
mn === nothing && return true
122121

122+
# 1) If the scope is a module, allow resolving the module name itself
123+
if CSTParser.defines_module(scope.expr)
124+
n = CSTParser.get_name(scope.expr)
125+
if CSTParser.isidentifier(n) && mn == CSTParser.valof(n)
126+
b = bindingof(scope.expr) # module’s binding
127+
if b isa Binding
128+
setref!(x, b)
129+
return true
130+
end
131+
end
132+
end
133+
134+
# 2) Resolve exported names from this module scope
123135
if scope_exports(scope, mn, state)
124136
setref!(x, scope.names[mn])
125-
resolved = true
137+
return true
126138
end
127-
return resolved
139+
140+
return false
128141
end
129142

130143
"""

test/runtests.jl

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,54 @@ function check_resolved(s)
2323
[(refof(i) !== nothing) for i in IDs]
2424
end
2525

26+
# Simple iterative DFS utilities (no recursive predicate calls)
27+
function module_name(ex::CSTParser.EXPR)::Union{String,Nothing}
28+
if CSTParser.defines_module(ex)
29+
n = CSTParser.get_name(ex)
30+
if CSTParser.isidentifier(n)
31+
return CSTParser.valof(n)
32+
elseif StaticLint.headof(n) === :NONSTDIDENTIFIER && length(n.args) == 2
33+
return CSTParser.valof(n.args[2])
34+
end
35+
end
36+
return nothing
37+
end
38+
39+
function find_module_by_name(root::CSTParser.EXPR, name::String)
40+
stack = CSTParser.EXPR[root]
41+
while !isempty(stack)
42+
x = pop!(stack)
43+
if module_name(x) == name
44+
return x
45+
end
46+
if x.args !== nothing
47+
# push children
48+
for a in x.args
49+
a isa CSTParser.EXPR && push!(stack, a)
50+
end
51+
end
52+
end
53+
return nothing
54+
end
55+
56+
function find_first(root::CSTParser.EXPR, f::Function)
57+
stack = CSTParser.EXPR[root]
58+
while !isempty(stack)
59+
x = pop!(stack)
60+
if f(x)
61+
return x
62+
end
63+
if x.args !== nothing
64+
for a in x.args
65+
a isa CSTParser.EXPR && push!(stack, a)
66+
end
67+
end
68+
end
69+
return nothing
70+
end
71+
# Adapter to support weird block call
72+
find_first(f::Function, root::CSTParser.EXPR) = find_first(root, f)
73+
2674
@testset "StaticLint" begin
2775

2876
@testset "Basic bindings" begin
@@ -1485,6 +1533,75 @@ f(arg) = arg
14851533
end""")
14861534
@test bindingof(cst.args[3].args[1].args[2]).type !== nothing
14871535
end
1536+
1537+
# @testset "forward relative using/import" begin
1538+
# cst = parse_and_pass("""
1539+
# module A
1540+
# module B
1541+
# module C
1542+
# using ..Sibling
1543+
# f() = Sibling.g()
1544+
# end
1545+
# module Sibling
1546+
# export g
1547+
# g() = 1
1548+
# end
1549+
# end
1550+
# end
1551+
# """)
1552+
# # f’s body Sibling.g should resolve
1553+
# fcall = cst.args[1].args[3].args[1].args[3].args[2] # C’s f() definition
1554+
# # Sibling.g call: fcall.args[2].args[1] is the call; its callee is getfield
1555+
# callee = fcall.args[2].args[1].args[1] # Sibling
1556+
# @test StaticLint.hasref(callee)
1557+
# end
1558+
1559+
@testset "forward relative using/import" begin
1560+
cst = parse_and_pass("""
1561+
module A
1562+
module B
1563+
module C
1564+
using ..Sibling
1565+
f() = Sibling.g()
1566+
end
1567+
module Sibling
1568+
export g
1569+
g() = 1
1570+
end
1571+
end
1572+
end
1573+
""")
1574+
1575+
modC = find_module_by_name(cst, "C")
1576+
@test modC !== nothing
1577+
1578+
fexpr = find_first(modC) do x
1579+
CSTParser.defines_function(x) &&
1580+
CSTParser.isidentifier(CSTParser.get_name(x)) &&
1581+
CSTParser.valof(CSTParser.get_name(x)) == "f"
1582+
end
1583+
@test fexpr !== nothing
1584+
1585+
gget = find_first(fexpr, CSTParser.is_getfield_w_quotenode)
1586+
@test gget !== nothing
1587+
1588+
lhs = gget.args[1] # Sibling
1589+
rhsid = gget.args[2].args[1] # g (inside QuoteNode)
1590+
1591+
@test StaticLint.hasref(lhs)
1592+
@test StaticLint.hasref(rhsid)
1593+
end
1594+
1595+
@testset "too many dots" begin
1596+
cst = parse_and_pass("""
1597+
module A
1598+
import ....X
1599+
end
1600+
""")
1601+
errs = StaticLint.collect_hints(cst, getenv(server.files[""], server))
1602+
@test any(err -> StaticLint.errorof(err[2]) === StaticLint.RelativeImportTooManyDots, errs)
1603+
end
1604+
14881605
end
14891606

14901607
@testset "add eval method to modules/toplevel scope" begin

0 commit comments

Comments
 (0)