@@ -206,7 +206,129 @@ end
206206# Temporary alias until Documenter updates
207207const softscope! = softscope
208208
209- const repl_ast_transforms = Any[softscope] # defaults for new REPL backends
209+ function print_qualified_access_warning (mod:: Module , owner:: Module , name:: Symbol )
210+ @warn string (name, " is defined in " , owner, " and is not public in " , mod) maxlog = 1 _id = string (" repl-warning-" , mod, " -" , owner, " -" , name) _line = nothing _file = nothing _module = nothing
211+ end
212+
213+ function has_ancestor (query:: Module , target:: Module )
214+ query == target && return true
215+ while true
216+ next = parentmodule (query)
217+ next == target && return true
218+ next == query && return false
219+ query = next
220+ end
221+ end
222+
223+ retrieve_modules (:: Module , :: Any ) = (nothing ,)
224+ function retrieve_modules (current_module:: Module , mod_name:: Symbol )
225+ mod = try
226+ getproperty (current_module, mod_name)
227+ catch
228+ return (nothing ,)
229+ end
230+ return (mod isa Module ? mod : nothing ,)
231+ end
232+ retrieve_modules (current_module:: Module , mod_name:: QuoteNode ) = retrieve_modules (current_module, mod_name. value)
233+ function retrieve_modules (current_module:: Module , mod_expr:: Expr )
234+ if Meta. isexpr (mod_expr, :., 2 )
235+ current_module = retrieve_modules (current_module, mod_expr. args[1 ])[1 ]
236+ current_module === nothing && return (nothing ,)
237+ return (current_module, retrieve_modules (current_module, mod_expr. args[2 ])... )
238+ else
239+ return (nothing ,)
240+ end
241+ end
242+
243+ add_locals! (locals, ast:: Any ) = nothing
244+ function add_locals! (locals, ast:: Expr )
245+ for arg in ast. args
246+ add_locals! (locals, arg)
247+ end
248+ return nothing
249+ end
250+ function add_locals! (locals, ast:: Symbol )
251+ push! (locals, ast)
252+ return nothing
253+ end
254+
255+ function collect_names_to_warn! (warnings, locals, current_module:: Module , ast)
256+ ast isa Expr || return
257+
258+ # don't recurse through module definitions
259+ ast. head === :module && return
260+
261+ if Meta. isexpr (ast, :., 2 )
262+ mod_name, name_being_accessed = ast. args
263+ # retrieve the (possibly-nested) module being named here
264+ mods = retrieve_modules (current_module, mod_name)
265+ all (x -> x isa Module, mods) || return
266+ outer_mod = first (mods)
267+ mod = last (mods)
268+ if name_being_accessed isa QuoteNode
269+ name_being_accessed = name_being_accessed. value
270+ end
271+ name_being_accessed isa Symbol || return
272+ owner = try
273+ which (mod, name_being_accessed)
274+ catch
275+ return
276+ end
277+ # if `owner` is a submodule of `mod`, then don't warn. E.g. the name `parse` is present in the module `JSON`
278+ # but is owned by `JSON.Parser`; we don't warn if it is accessed as `JSON.parse`.
279+ has_ancestor (owner, mod) && return
280+ # Don't warn if the name is public in the module we are accessing it
281+ Base. ispublic (mod, name_being_accessed) && return
282+ # Don't warn if accessing names defined in Core from Base if they are present in Base (e.g. `Base.throw`).
283+ mod === Base && Base. ispublic (Core, name_being_accessed) && return
284+ push! (warnings, (; outer_mod, mod, owner, name_being_accessed))
285+ # no recursion
286+ return
287+ elseif Meta. isexpr (ast, :(= ), 2 )
288+ lhs, rhs = ast. args
289+ # any symbols we find on the LHS we will count as local. This can potentially be overzealous,
290+ # but we want to avoid false positives (unnecessary warnings) more than false negatives.
291+ add_locals! (locals, lhs)
292+ # we'll recurse into the RHS only
293+ return collect_names_to_warn! (warnings, locals, current_module, rhs)
294+ elseif Meta. isexpr (ast, :function ) && length (ast. args) >= 1
295+
296+ if Meta. isexpr (ast. args[1 ], :call , 2 )
297+ func_name, func_args = ast. args[1 ]. args
298+ # here we have a function definition and are inspecting it's arguments for local variables.
299+ # we will error on the conservative side by adding all symbols we find (regardless if they are local variables or possibly-global default values)
300+ add_locals! (locals, func_args)
301+ end
302+ # fall through to general recursion
303+ end
304+
305+ for arg in ast. args
306+ collect_names_to_warn! (warnings, locals, current_module, arg)
307+ end
308+
309+ return nothing
310+ end
311+
312+ function collect_qualified_access_warnings (current_mod, ast)
313+ warnings = Set ()
314+ locals = Set {Symbol} ()
315+ collect_names_to_warn! (warnings, locals, current_mod, ast)
316+ filter! (warnings) do (; outer_mod)
317+ nameof (outer_mod) ∉ locals
318+ end
319+ return warnings
320+ end
321+
322+ function warn_on_non_owning_accesses (current_mod, ast)
323+ warnings = collect_qualified_access_warnings (current_mod, ast)
324+ for (; outer_mod, mod, owner, name_being_accessed) in warnings
325+ print_qualified_access_warning (mod, owner, name_being_accessed)
326+ end
327+ return ast
328+ end
329+ warn_on_non_owning_accesses (ast) = warn_on_non_owning_accesses (REPL. active_module (), ast)
330+
331+ const repl_ast_transforms = Any[softscope, warn_on_non_owning_accesses] # defaults for new REPL backends
210332
211333# Allows an external package to add hooks into the code loading.
212334# The hook should take a Vector{Symbol} of package names and
0 commit comments