Skip to content

Commit de31a82

Browse files
committed
Fix definition(String, m) for methods defined at the REPL
Fixes #28. Note this revealed a number of other concerns, most notably that stripping the first and last character made strong assumptions about what those characters were. This now just strips leading and trailing '\n', which is technically a breaking change.
1 parent 98c80e9 commit de31a82

File tree

3 files changed

+48
-9
lines changed

3 files changed

+48
-9
lines changed

src/CodeTracking.jl

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ Returns `nothing` if there are no methods at that location.
115115
"""
116116
function signatures_at(filename::AbstractString, line::Integer)
117117
if !startswith(filename, "REPL[")
118-
filename = abspath(filename)
118+
filename = abspath(filename)
119119
end
120120
if occursin(juliabase, filename)
121121
rpath = postpath(filename, juliabase)
@@ -188,17 +188,18 @@ see [`definition(Expr, method::Method)`](@ref) instead.
188188
"""
189189
function definition(::Type{String}, method::Method)
190190
file, line = whereis(method)
191-
src = read(file, String)
191+
src = src_from_file_or_REPL(file)
192192
eol = isequal('\n')
193193
linestarts = Int[]
194-
istart = 0
194+
istart = 1
195195
for i = 1:line-1
196-
push!(linestarts, istart+1)
197-
istart = findnext(eol, src, istart+1)
196+
push!(linestarts, istart)
197+
istart = findnext(eol, src, istart) + 1
198198
end
199199
ex, iend = Meta.parse(src, istart)
200200
if isfuncexpr(ex)
201-
return src[istart+1:iend-1], line
201+
iend = min(iend, lastindex(src))
202+
return strip(src[istart:iend], '\n'), line
202203
end
203204
# The function declaration was presumably on a previous line
204205
lineindex = lastindex(linestarts)

src/utils.jl

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,35 @@ fileline(lnn::LineNumberNode) = String(lnn.file), lnn.line
3939
# This is piracy, but it's not ambiguous in terms of what it should do
4040
Base.convert(::Type{LineNumberNode}, lin::LineInfoNode) = LineNumberNode(lin.line, lin.file)
4141

42+
# This regex matches the pseudo-file name of a REPL history entry.
43+
const rREPL = r"^REPL\[(\d+)\]$"
44+
45+
"""
46+
src = src_from_file_or_REPL(origin::AbstractString, repl = Base.active_repl)
47+
48+
Read the source for a function from `origin`, which is either the name of a file
49+
or "REPL[\$i]", where `i` is an integer specifying the particular history entry.
50+
Methods defined at the REPL use strings of this form in their `file` field.
51+
52+
If you happen to have a file where the name matches `REPL[\$i]`, first pass it through
53+
`abspath`.
54+
"""
55+
function src_from_file_or_REPL(origin::AbstractString, args...)
56+
# This Varargs design prevents an unnecessary error when Base.active_repl is undefined
57+
# and `origin` does not match "REPL[$i]"
58+
m = match(rREPL, origin)
59+
if m !== nothing
60+
return _src_from_file_or_REPL(origin, args...)
61+
end
62+
return read(origin, String)
63+
end
64+
65+
function _src_from_file_or_REPL(origin::AbstractString, repl = Base.active_repl)
66+
hist_idx = parse(Int, m.captures[1])
67+
hp = repl.interface.modes[1].hist
68+
return hp.history[hp.start_idx+hist_idx]
69+
end
70+
4271
function basepath(id::PkgId)
4372
id.name ("Main", "Base", "Core") && return ""
4473
loc = Base.locate_package(id)

test/runtests.jl

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,7 @@ isdefined(Main, :Revise) ? includet("script.jl") : include("script.jl")
3131

3232
m = first(methods(f2))
3333
src, line = definition(String, m)
34-
@test src == """
35-
f2(x, y) = x + y
36-
"""
34+
@test src == "f2(x, y) = x + y"
3735
@test line == 7
3836

3937
info = CodeTracking.PkgFiles(Base.PkgId(CodeTracking))
@@ -62,6 +60,17 @@ isdefined(Main, :Revise) ? includet("script.jl") : include("script.jl")
6260
# Test with broken lookup
6361
CodeTracking.method_lookup_callback[] = m -> error("oops")
6462
@test whereis(m) == ("REPL[1]", 1)
63+
# Test with definition(String, m)
64+
if isdefined(Base, :active_repl)
65+
hp = Base.active_repl.interface.modes[1].hist
66+
fstr = "__fREPL__(x::Int16) = 0"
67+
histidx = length(hp.history) + 1 - hp.start_idx
68+
ex = Base.parse_input_line(fstr; filename="REPL[$histidx]")
69+
f = Core.eval(Main, ex)
70+
push!(hp.history, fstr)
71+
@test definition(String, first(methods(f))) == (fstr, 1)
72+
pop!(hp.history)
73+
end
6574

6675
m = first(methods(Test.eval))
6776
@test occursin(Sys.STDLIB, whereis(m)[1])

0 commit comments

Comments
 (0)