Skip to content

Commit f98b7dd

Browse files
authored
Upgrade JuliaSyntax to v1 (#110)
* wip: upgrade to JuliaSyntax v1 * gitignore for ]dev --local * undo simplify hashing hack * pkg roundtrip * improve debugging * change for quoting in qualified names (JS#324) * wip: hashing fix * switch to object identity * fix for and generator iteration * stricter * inline function defs do not have K"=" anymore * fix do-blocks * bump version * support new error on 1.12 * support module aliases * format tests & wrap in testset * re-enable aqua * support 1.12-beta * format
1 parent ebbb403 commit f98b7dd

File tree

5 files changed

+94
-37
lines changed

5 files changed

+94
-37
lines changed

Project.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "ExplicitImports"
22
uuid = "7d51a73a-1435-4ff3-83d9-f097790105c7"
3-
version = "1.11.3"
43
authors = ["Eric P. Hanson"]
4+
version = "1.12.0"
55

66
[deps]
77
AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c"
@@ -17,7 +17,7 @@ AbstractTrees = "0.4.5"
1717
Aqua = "0.8.4"
1818
Compat = "4.15"
1919
DataFrames = "1.6"
20-
JuliaSyntax = "0.4.8"
20+
JuliaSyntax = "1"
2121
LinearAlgebra = "<0.0.1, 1"
2222
Logging = "<0.0.1, 1"
2323
Markdown = "<0.0.1, 1"

src/get_names_used.jl

Lines changed: 61 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ Base.@kwdef struct PerUsageInfo
2323
analysis_code::AnalysisCode
2424
end
2525

26+
function Base.show(io::IO, r::PerUsageInfo)
27+
return print(io,
28+
"PerUsageInfo (`$(r.name)` @ $(r.location), `qualified_by`=$(r.qualified_by))")
29+
end
30+
2631
function Base.NamedTuple(r::PerUsageInfo)
2732
names = fieldnames(typeof(r))
2833
return NamedTuple{names}(map(x -> getfield(r, x), names))
@@ -53,12 +58,18 @@ end
5358

5459
# returns `nothing` for no qualifying module, otherwise a symbol
5560
function qualifying_module(leaf)
61+
@debug "[qualifying_module] leaf: $(js_node(leaf)) start"
62+
# introspect leaf and its tree of parents
63+
@debug "[qualifying_module] leaf: $(js_node(leaf)) parents: $(parent_kinds(leaf))"
64+
5665
# is this name being used in a qualified context, like `X.y`?
57-
parents_match(leaf, (K"quote", K".")) || return nothing
66+
parents_match(leaf, (K".",)) || return nothing
67+
@debug "[qualifying_module] leaf: $(js_node(leaf)) passed dot"
5868
# Are we on the right-hand side?
59-
child_index(parent(leaf)) == 2 || return nothing
69+
child_index(leaf) == 2 || return nothing
70+
@debug "[qualifying_module] leaf: $(js_node(leaf)) passed right-hand side"
6071
# Ok, now try to retrieve the child on the left-side
61-
node = first(AbstractTrees.children(get_parent(leaf, 2)))
72+
node = first(AbstractTrees.children(parent(leaf)))
6273
path = Symbol[]
6374
retrieve_module_path!(path, node)
6475
return path
@@ -139,8 +150,8 @@ function is_anonymous_do_function_definition_arg(leaf)
139150
if !has_parent(leaf, 2)
140151
return false
141152
elseif parents_match(leaf, (K"tuple", K"do"))
142-
# second argument of `do`-block
143-
return child_index(parent(leaf)) == 2
153+
# first argument of `do`-block (args then function body since JuliaSyntax 1.0)
154+
return child_index(parent(leaf)) == 1
144155
elseif kind(parent(leaf)) in (K"tuple", K"parameters")
145156
# Ok, let's just step up one level and see again
146157
return is_anonymous_do_function_definition_arg(parent(leaf))
@@ -231,10 +242,6 @@ function call_is_func_def(node)
231242
# note: macros only support full-form function definitions
232243
# (not inline)
233244
kind(p) in (K"function", K"macro") && return true
234-
if kind(p) == K"="
235-
# call should be the first arg in an inline function def
236-
return child_index(node) == 1
237-
end
238245
return false
239246
end
240247

@@ -268,11 +275,18 @@ end
268275
# https://github.com/JuliaLang/JuliaSyntax.jl/issues/432
269276
function in_for_argument_position(node)
270277
# We must be on the LHS of a `for` `equal`.
271-
if !has_parent(node, 2)
278+
if !has_parent(node, 3)
272279
return false
273-
elseif parents_match(node, (K"=", K"for"))
274-
return child_index(node) == 1
275-
elseif parents_match(node, (K"=", K"cartesian_iterator", K"for"))
280+
elseif parents_match(node, (K"in", K"iteration", K"for"))
281+
@debug """
282+
[in_for_argument_position] node: $(js_node(node))
283+
parents: $(parent_kinds(node))
284+
child_index=$(child_index(node))
285+
parent_child_index=$(child_index(get_parent(node, 1)))
286+
parent_child_index2=$(child_index(get_parent(node, 2)))
287+
"""
288+
289+
# child_index(node) == 1 means we are the first argument of the `in`, like `yi in y`
276290
return child_index(node) == 1
277291
elseif kind(parent(node)) in (K"tuple", K"parameters")
278292
return in_for_argument_position(get_parent(node))
@@ -293,13 +307,11 @@ end
293307

294308
function in_generator_arg_position(node)
295309
# We must be on the LHS of a `=` inside a generator
296-
# (possibly inside a filter, possibly inside a `cartesian_iterator`)
297-
if !has_parent(node, 2)
310+
# (possibly inside a filter, possibly inside a `iteration`)
311+
if !has_parent(node, 3)
298312
return false
299-
elseif parents_match(node, (K"=", K"generator")) ||
300-
parents_match(node, (K"=", K"cartesian_iterator", K"generator")) ||
301-
parents_match(node, (K"=", K"filter")) ||
302-
parents_match(node, (K"=", K"cartesian_iterator", K"filter"))
313+
elseif parents_match(node, (K"in", K"iteration", K"generator")) ||
314+
parents_match(node, (K"in", K"iteration", K"filter"))
303315
return child_index(node) == 1
304316
elseif kind(parent(node)) in (K"tuple", K"parameters")
305317
return in_generator_arg_position(get_parent(node))
@@ -356,16 +368,13 @@ function analyze_name(leaf; debug=false)
356368
# update our state
357369
val = get_val(node)
358370
k = kind(node)
359-
args = nodevalue(node).node.raw.args
371+
args = nodevalue(node).node.raw.children
360372

361373
debug && println(val, ": ", k)
362374
# Constructs that start a new local scope. Note `let` & `macro` *arguments* are not explicitly supported/tested yet,
363375
# but we can at least keep track of scope properly.
364376
if k in
365-
(K"let", K"for", K"function", K"struct", K"generator", K"while", K"macro") ||
366-
# Or do-block when we are considering a path that did not go through the first-arg
367-
# (which is the function name, and NOT part of the local scope)
368-
(k == K"do" && child_index(prev_node) > 1) ||
377+
(K"let", K"for", K"function", K"struct", K"generator", K"while", K"macro", K"do") ||
369378
# any child of `try` gets it's own individual scope (I think)
370379
(parents_match(node, (K"try",)))
371380
push!(scope_path, nodevalue(node).node)
@@ -506,7 +515,7 @@ function is_name_internal_in_higher_local_scope(name, scope_path, seen)
506515
end
507516
# Ok, now pop off the first scope and check.
508517
scope_path = scope_path[2:end]
509-
ret = get(seen, (; name, scope_path), nothing)
518+
ret = get(seen, (; name, scope_path=SyntaxNodeList(scope_path)), nothing)
510519
if ret === nothing
511520
# Not introduced here yet, trying recursing further
512521
continue
@@ -519,6 +528,25 @@ function is_name_internal_in_higher_local_scope(name, scope_path, seen)
519528
return false
520529
end
521530

531+
# We implement a workaround for https://github.com/JuliaLang/JuliaSyntax.jl/issues/558
532+
# Hashing and equality for SyntaxNodes were changed from object identity to a recursive comparison
533+
# in JuliaSyntax 1.0. This is very slow and also not quite the semantics we want anyway.
534+
# Here, we wrap our nodes in a custom type that only compares object identity.
535+
struct SyntaxNodeList
536+
nodes::Vector{JuliaSyntax.SyntaxNode}
537+
end
538+
539+
function Base.:(==)(a::SyntaxNodeList, b::SyntaxNodeList)
540+
return map(objectid, a.nodes) == map(objectid, b.nodes)
541+
end
542+
function Base.isequal(a::SyntaxNodeList, b::SyntaxNodeList)
543+
return isequal(map(objectid, a.nodes), map(objectid, b.nodes))
544+
end
545+
546+
function Base.hash(a::SyntaxNodeList, h::UInt)
547+
return hash(map(objectid, a.nodes), h)
548+
end
549+
522550
function analyze_per_usage_info(per_usage_info)
523551
# For each scope, we want to understand if there are any global usages of the name in that scope
524552
# First, throw away all qualified usages, they are irrelevant
@@ -527,9 +555,9 @@ function analyze_per_usage_info(per_usage_info)
527555
# Otherwise, we are in local scope:
528556
# 1. Next, if the name is a function arg, then this is not a global name (essentially first usage is assignment)
529557
# 2. Otherwise, if first usage is assignment, then it is local, otherwise it is global
530-
seen = Dict{@NamedTuple{name::Symbol,scope_path::Vector{JuliaSyntax.SyntaxNode}},Bool}()
558+
seen = Dict{@NamedTuple{name::Symbol,scope_path::SyntaxNodeList},Bool}()
531559
return map(per_usage_info) do nt
532-
@compat if (; nt.name, nt.scope_path) in keys(seen)
560+
@compat if (; nt.name, scope_path=SyntaxNodeList(nt.scope_path)) in keys(seen)
533561
return PerUsageInfo(; nt..., first_usage_in_scope=false,
534562
external_global_name=missing,
535563
analysis_code=IgnoredNonFirst)
@@ -561,7 +589,8 @@ function analyze_per_usage_info(per_usage_info)
561589
(nt.is_assignment, InternalAssignment))
562590
if is_local
563591
external_global_name = false
564-
push!(seen, (; nt.name, nt.scope_path) => external_global_name)
592+
push!(seen,
593+
(; nt.name, scope_path=SyntaxNodeList(nt.scope_path)) => external_global_name)
565594
return PerUsageInfo(; nt..., first_usage_in_scope=true,
566595
external_global_name,
567596
analysis_code=reason)
@@ -572,13 +601,15 @@ function analyze_per_usage_info(per_usage_info)
572601
nt.scope_path,
573602
seen)
574603
external_global_name = false
575-
push!(seen, (; nt.name, nt.scope_path) => external_global_name)
604+
push!(seen,
605+
(; nt.name, scope_path=SyntaxNodeList(nt.scope_path)) => external_global_name)
576606
return PerUsageInfo(; nt..., first_usage_in_scope=true, external_global_name,
577607
analysis_code=InternalHigherScope)
578608
end
579609

580610
external_global_name = true
581-
push!(seen, (; nt.name, nt.scope_path) => external_global_name)
611+
push!(seen,
612+
(; nt.name, scope_path=SyntaxNodeList(nt.scope_path)) => external_global_name)
582613
return PerUsageInfo(; nt..., first_usage_in_scope=true, external_global_name,
583614
analysis_code=External)
584615
end

src/improper_qualified_accesses.jl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@ function analyze_qualified_names(mod::Module, file=pathof(mod);
1111
# something there would invalidate the qualified names with issues we did find.
1212
# For now let's ignore it.
1313

14+
@debug "[analyze_qualified_names] per_usage_info has $(length(per_usage_info)) rows"
1415
# Filter to qualified names
1516
qualified = [row for row in per_usage_info if row.qualified_by !== nothing]
1617

18+
@debug "[analyze_qualified_names] qualified has $(length(qualified)) rows"
19+
1720
# which are in our module
1821
mod_path = module_path(mod)
1922
match = module_path -> all(Base.splat(isequal), zip(module_path, mod_path))
@@ -54,7 +57,7 @@ end
5457
function process_qualified_row(row, mod)
5558
# for JET
5659
@assert !isnothing(row.qualified_by)
57-
60+
5861
isempty(row.qualified_by) && return nothing
5962
current_mod = mod
6063
for submod in row.qualified_by

src/parse_utilities.jl

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ AbstractTrees.children(::SkippedFile) = ()
5151
function AbstractTrees.children(wrapper::SyntaxNodeWrapper)
5252
node = wrapper.node
5353
if JuliaSyntax.kind(node) == K"call"
54-
children = JuliaSyntax.children(node)
54+
children = js_children(node)
5555
if length(children) == 2
5656
f, arg = children::Vector{JuliaSyntax.SyntaxNode} # make JET happy
5757
if f.val === :include
@@ -60,7 +60,7 @@ function AbstractTrees.children(wrapper::SyntaxNodeWrapper)
6060
return [SkippedFile(location)]
6161
end
6262
if JuliaSyntax.kind(arg) == K"string"
63-
children = JuliaSyntax.children(arg)
63+
children = js_children(arg)
6464
# if we have interpolation, there may be >1 child
6565
length(children) == 1 || @goto dynamic
6666
c = only(children)
@@ -92,10 +92,14 @@ function AbstractTrees.children(wrapper::SyntaxNodeWrapper)
9292
end
9393
end
9494
return map(n -> SyntaxNodeWrapper(n, wrapper.file, wrapper.bad_locations),
95-
JuliaSyntax.children(node))
95+
js_children(node))
9696
end
9797

98-
js_children(n::Union{TreeCursor,SyntaxNodeWrapper}) = JuliaSyntax.children(js_node(n))
98+
js_children(n::Union{TreeCursor,SyntaxNodeWrapper}) = js_children(js_node(n))
99+
100+
# https://github.com/JuliaLang/JuliaSyntax.jl/issues/557
101+
js_children(n::Union{JuliaSyntax.SyntaxNode}) = something(JuliaSyntax.children(n), ())
102+
99103
js_node(n::SyntaxNodeWrapper) = n.node
100104
js_node(n::TreeCursor) = js_node(nodevalue(n))
101105

@@ -135,6 +139,17 @@ function parents_match(n::TreeCursor, kinds::Tuple)
135139
return parents_match(p, Base.tail(kinds))
136140
end
137141

142+
143+
function parent_kinds(n::TreeCursor)
144+
kinds = []
145+
while true
146+
n = parent(n)
147+
n === nothing && return kinds
148+
push!(kinds, kind(n))
149+
end
150+
return kinds
151+
end
152+
138153
function get_parent(n, i=1)
139154
for _ in i:-1:1
140155
n = parent(n)

test/start_tests.jl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using TestEnv # assumed globally installed
2+
using Pkg
3+
Pkg.activate(joinpath(@__DIR__, ".."))
4+
TestEnv.activate()
5+
using ExplicitImports
6+
7+
cd(joinpath(pkgdir(ExplicitImports), "test"))
8+
include("runtests.jl")

0 commit comments

Comments
 (0)