Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
126 changes: 126 additions & 0 deletions src/get_names_used.jl
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,123 @@ function analyze_name(leaf; debug=false)
end
end

function cmdstring_parent(leaf)
node = leaf
while true
kind(node) == K"cmdstring" && return node
has_parent(node) || return nothing
node = parent(node)
end
end

function unwrap_toplevel_expr(expr)
if expr isa Expr && expr.head == :toplevel && length(expr.args) == 1
return expr.args[1]
end
return expr
end

function cmdstring_string_literal(expr)
expr = unwrap_toplevel_expr(expr)
if expr isa Expr && expr.head == :macrocall
for arg in expr.args[2:end]
arg isa LineNumberNode && continue
if arg isa String
return arg
elseif arg isa Expr && arg.head == :string
all(x -> x isa String, arg.args) || return nothing
return join(arg.args)
end
end
return nothing
elseif expr isa String
return expr
end
return nothing
end

function collect_shell_interpolations!(exprs, part)
if part isa Expr && part.head == :tuple
for item in part.args
collect_shell_interpolations!(exprs, item)
end
elseif part isa AbstractString
return nothing
else
push!(exprs, part)
end
return nothing
end

# Parse the full cmd literal once so interpolation extraction follows Julia's grammar.
function cmdstring_interpolations_from_source(cmd_src::AbstractString)
expr = Meta.parse(cmd_src; raise=false)
expr isa Expr && expr.head == :error && return Any[]
cmd_str = cmdstring_string_literal(expr)
cmd_str === nothing && return Any[]
parsed = try
Base.shell_parse(cmd_str; special=Base.shell_special)[1]
catch
return Any[]
end
exprs = Any[]
collect_shell_interpolations!(exprs, parsed)
return exprs
end

function append_cmdstring_interpolations!(per_usage_info, leaf, processed_cmdstrings)
cmd_node = cmdstring_parent(leaf)
cmd_node === nothing && return nothing
cmd_id = objectid(js_node(cmd_node))
if cmd_id in processed_cmdstrings
return nothing
end
push!(processed_cmdstrings, cmd_id)

cmd_src = String(JuliaSyntax.sourcetext(js_node(cmd_node)))
exprs = cmdstring_interpolations_from_source(cmd_src)
isempty(exprs) && return nothing

outer = analyze_name(leaf)
outer_scope_path = outer.scope_path
outer_module_path = outer.module_path
wrapper = nodevalue(leaf)
location = location_str(wrapper)

for expr in exprs
expr_src = sprint(Base.show_unquoted, expr)
parsed = JuliaSyntax.parseall(JuliaSyntax.SyntaxNode, expr_src; ignore_warnings=true)
expr_wrapper = SyntaxNodeWrapper(parsed, wrapper.file, wrapper.bad_locations)
cursor = TreeCursor(expr_wrapper)
for expr_leaf in Leaves(cursor)
if nodevalue(expr_leaf) isa SkippedFile
continue
end
(kind(expr_leaf) in (K"Identifier", K"MacroName", K"StringMacroName"))::Bool ||
continue
parents_match(expr_leaf, (K"quote",)) &&
!parents_match(expr_leaf, (K"quote", K".")) && continue

name = get_val(expr_leaf)
qualified_by = qualifying_module(expr_leaf)
import_type = analyze_import_type(expr_leaf)
explicitly_imported_by = import_type == :import_RHS ? get_import_lhs(expr_leaf) : nothing
inner = analyze_name(expr_leaf)
scope_path = vcat(inner.scope_path, outer_scope_path)
push!(per_usage_info,
(; name,
qualified_by,
import_type,
explicitly_imported_by,
location,
inner...,
module_path=outer_module_path,
scope_path))
end
end
return outer_module_path
end

"""
analyze_all_names(file)

Expand Down Expand Up @@ -813,6 +930,7 @@ function analyze_all_names(file)
# safe to analyze.
seen_modules = Set{Vector{Symbol}}()
tainted_modules = Set{Vector{Symbol}}()
processed_cmdstrings = Set{UInt}()

for leaf in Leaves(cursor)
if nodevalue(leaf) isa SkippedFile
Expand All @@ -822,6 +940,14 @@ function analyze_all_names(file)
continue
end

if kind(leaf) == K"CmdString"
mod_path = append_cmdstring_interpolations!(per_usage_info,
leaf,
processed_cmdstrings)
mod_path === nothing || push!(seen_modules, mod_path)
continue
end

# if we don't find any identifiers (or macro names) in a module, I think it's OK to mark it as
# "not-seen"? Otherwise we need to analyze every leaf, not just the identifiers
# and that sounds slow. Seems like a very rare edge case to have no identifiers...
Expand Down
60 changes: 60 additions & 0 deletions test/issue_81.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
module UnzipExporter81

export unzip

unzip() = "unzip"

end # module UnzipExporter81

module CmdInterpolationUsesImport

using ..UnzipExporter81: unzip

function register_steelProfile()
function post_fetch_method(file)
run(`$(unzip()) -q $file`)
rm(file)
end
end

end # module CmdInterpolationUsesImport

module CmdInterpolationUsesImportQuoted

using ..UnzipExporter81: unzip

function register_quoted(file)
run(`"$(unzip())" -q "$file"`)
end

end # module CmdInterpolationUsesImportQuoted

module CmdInterpolationUsesImportNested

using ..UnzipExporter81: unzip

function register_nested(file)
run(`$(unzip()) -q $(joinpath(dirname(file), basename(file)))`)
end

end # module CmdInterpolationUsesImportNested

module CmdInterpolationUsesImportAdjacent

using ..UnzipExporter81: unzip

function register_adjacent(file)
run(`$(unzip()) --dest=$(basename(file))_out`)
end

end # module CmdInterpolationUsesImportAdjacent

module CmdInterpolationUsesImportQuotedLiteral

using ..UnzipExporter81: unzip

function register_quoted_literal(file)
run(`echo '$unzip' $file`)
end

end # module CmdInterpolationUsesImportQuotedLiteral
15 changes: 15 additions & 0 deletions test/issue_81_test.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Test
using ExplicitImports

issue_path = joinpath(@__DIR__, "issue_81.jl")
include(issue_path)

@testset "Cmd interpolation uses explicit imports (#81)" begin
@test check_no_stale_explicit_imports(CmdInterpolationUsesImport, issue_path) === nothing
@test check_no_stale_explicit_imports(CmdInterpolationUsesImportQuoted, issue_path) === nothing
@test check_no_stale_explicit_imports(CmdInterpolationUsesImportNested, issue_path) === nothing
@test check_no_stale_explicit_imports(CmdInterpolationUsesImportAdjacent, issue_path) === nothing
@test_throws ExplicitImports.StaleImportsException check_no_stale_explicit_imports(
CmdInterpolationUsesImportQuotedLiteral,
issue_path)
end
4 changes: 4 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ include("issue_140.jl")
include("issue_111_test.jl")
end

@testset "Cmd interpolation uses explicit imports (#81)" begin
include("issue_81_test.jl")
end

@testset "Macro explicit imports (#97)" begin
include("issue_97_test.jl")
end
Expand Down
Loading