Skip to content
Merged
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
25 changes: 25 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
language: julia

os:
- linux
- osx

julia:
- 1.0
- 1.1
- nightly

branches:
only:
- master
- /^v\d+\.\d+(\.\d+)?(-\S*)?$/

notifications:
email: false

script:
- export JULIA_PROJECT=""
- julia --project -e 'using Pkg; Pkg.build(); Pkg.test();'

after_success:
- julia -e 'import Pkg; Pkg.add("Coverage"); using Coverage; Codecov.submit(Codecov.process_folder())'
6 changes: 5 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ uuid = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2"
authors = ["Tim Holy <tim.holy@gmail.com>"]
version = "0.1.0"

[deps]
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"

[extras]
ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test"]
test = ["Test", "ColorTypes"]
38 changes: 29 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

CodeTracking is a minimal package designed to work with (a future version of)
[Revise.jl](https://github.com/timholy/Revise.jl).
Its main purpose is to support packages that need to know the location
(file and line number) of code that might move around as it's edited.
Its main purpose is to support packages that need to interact with code that might move
around as it's edited.

CodeTracking is a very lightweight dependency.

Expand All @@ -23,6 +23,29 @@ In this (ficticious) example, `sum` moved because I deleted a few lines higher i
these didn't affect the functionality of `sum` (so we didn't need to redefine and recompile it),
but it does change the starting line number of the file at which this method appears.

Other features:

```julia
julia> using CodeTracking, ColorTypes

julia> pkgfiles(ColorTypes)
PkgFiles(ColorTypes [3da002f7-5984-5a60-b8a6-cbb66c0b333f]):
basedir: /home/tim/.julia/packages/ColorTypes/BsAWO
files: ["src/ColorTypes.jl", "src/types.jl", "src/traits.jl", "src/conversions.jl", "src/show.jl", "src/operations.jl"]

julia> m = @which red(RGB(1,1,1))
red(c::AbstractRGB) in ColorTypes at /home/tim/.julia/packages/ColorTypes/BsAWO/src/traits.jl:14

julia> definition(m)
:(red(c::AbstractRGB) = begin
#= /home/tim/.julia/packages/ColorTypes/BsAWO/src/traits.jl:14 =#
c.r
end)

julia> definition(m, String)
"red(c::AbstractRGB ) = c.r\n"
```

## A few details

CodeTracking won't do anything *useful* unless the user is also running Revise,
Expand All @@ -32,15 +55,12 @@ file/line info in the method itself if Revise isn't running.)

However, Revise is a fairly large (and fairly complex) package, and currently it's not
easy to discover how to extract particular kinds of information from its internal storage.
CodeTracking will be designed to be the new "query" part of Revise.jl.
CodeTracking is designed to be the new "query" part of Revise.jl.
The aim is to have a very simple API that developers can learn in a few minutes and then
incorporate into their own packages; its lightweight nature means that they potentially gain
a lot of functionality without being forced to take a big hit in startup time.

## Current state
## Status

Currently this package is just a stub---it doesn't do anything useful,
but neither should it hurt anything.
Candidate users may wish to start `import`ing it and then file issues
or submit PRs as they discover what kinds of functionality they need
from CodeTracking.
If you want CodeTracking to do anything useful, currently you have to check out the `teh/codetracking`
branch of Revise.
102 changes: 98 additions & 4 deletions src/CodeTracking.jl
Original file line number Diff line number Diff line change
@@ -1,15 +1,109 @@
module CodeTracking

export whereis
using Base: PkgId
using Core: LineInfoNode
using UUIDs

# This is just a stub implementation for now
export PkgFiles
export whereis, definition, pkgfiles

include("data.jl")
include("utils.jl")

"""
filepath, line = whereis(method::Method)
Return the file and line of the definition of `method`. `line`
is the first line of the method's body.
"""
function whereis(method::Method)
file, line = String(method.file), method.line
lin = get(method_locations, method.sig, nothing)
if lin === nothing
file, line = String(method.file), method.line
else
file, line = fileline(lin)
end
if !isabspath(file)
# This is a Base method
# This is a Base or Core method
file = Base.find_source_file(file)
end
return normpath(file), line
end

"""
src = definition(method::Method, String)
Return a string with the code that defines `method`.
Note this may not be terribly useful for methods that are defined inside `@eval` statements;
see [`definition(method::Method, Expr)`](@ref) instead.
"""
function definition(method::Method, ::Type{String})
file, line = whereis(method)
src = read(file, String)
eol = isequal('\n')
linestarts = Int[]
istart = 0
for i = 1:line-1
push!(linestarts, istart+1)
istart = findnext(eol, src, istart+1)
end
ex, iend = Meta.parse(src, istart)
if isfuncexpr(ex)
return src[istart+1:iend-1]
end
# The function declaration was presumably on a previous line
lineindex = lastindex(linestarts)
while !isfuncexpr(ex)
istart = linestarts[lineindex]
ex, iend = Meta.parse(src, istart)
end
return src[istart:iend-1]
end

"""
ex = definition(method::Method, Expr)
ex = definition(method::Method)
Return an expression that defines `method`.
"""
function definition(method::Method, ::Type{Expr})
def = get(method_definitions, method.sig, nothing)
if def === nothing
f = method_lookup_callback[]
if f !== nothing
Base.invokelatest(f, method)
end
def = get(method_definitions, method.sig, nothing)
end
return def === nothing ? nothing : copy(def)
end

definition(method::Method) = definition(method, Expr)

"""
info = pkgfiles(name::AbstractString)
info = pkgfiles(name::AbstractString, uuid::UUID)
Return a [`PkgFiles`](@ref) structure with information about the files that define the package
specified by `name` and `uuid`.
Returns `nothing` if this package has not been loaded.
"""
pkgfiles(name::AbstractString, uuid::UUID) = pkgfiles(PkgId(uuid, name))
function pkgfiles(name::AbstractString)
project = Base.active_project()
uuid = Base.project_deps_get(project, name)
uuid == false && error("no package ", name, " recognized")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to error here or just return nothing like when it's a real installed package that just hasn't been loaded yet?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Erroring seems like a good choice to me. And it is at least the conservative choice which can be relaxed later.

return pkgfiles(name, uuid)
end
pkgfiles(id::PkgId) = get(_pkgfiles, id, nothing)

"""
info = pkgfiles(mod::Module)
Return a [`PkgFiles`](@ref) structure with information about the files that were loaded to
define the package that defined `mod`.
"""
pkgfiles(mod::Module) = pkgfiles(PkgId(mod))

end # module
42 changes: 42 additions & 0 deletions src/data.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# The variables here get populated by Revise.jl.

"""
PkgFiles encodes information about the current location of a package.
Fields:
- `id`: the `PkgId` of the package
- `basedir`: the current base directory of the package
- `files`: a list of files (relative path to `basedir`) that define the package.

Note that `basedir` may be subsequently updated by Pkg operations such as `add` and `dev`.
"""
mutable struct PkgFiles
id::PkgId
basedir::String
files::Vector{String}
end

PkgFiles(id::PkgId, path::AbstractString) = PkgFiles(id, path, String[])
PkgFiles(id::PkgId, ::Nothing) = PkgFiles(id, "")
PkgFiles(id::PkgId) = PkgFiles(id, normpath(basepath(id)))
PkgFiles(id::PkgId, files::AbstractVector{<:AbstractString}) =
PkgFiles(id, normpath(basepath(id)), files)

# Abstraction interface
Base.PkgId(info::PkgFiles) = info.id
srcfiles(info::PkgFiles) = info.files
basedir(info::PkgFiles) = info.basedir

function Base.show(io::IO, info::PkgFiles)
println(io, "PkgFiles(", info.id, "):")
println(io, " basedir: ", info.basedir)
print(io, " files: ")
show(io, info.files)
end

const method_locations = IdDict{Type,LineInfoNode}()

const method_definitions = IdDict{Type,Expr}()

const _pkgfiles = Dict{PkgId,PkgFiles}()

const method_lookup_callback = Ref{Any}(nothing)
23 changes: 23 additions & 0 deletions src/utils.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
function isfuncexpr(ex)
ex.head == :function && return true
if ex.head == :(=)
a = ex.args[1]
if isa(a, Expr)
while a.head == :where
a = a.args[1]
isa(a, Expr) || return false
end
a.head == :call && return true
end
end
return false
end

fileline(lin::LineInfoNode) = String(lin.file), lin.line

function basepath(id::PkgId)
id.name ∈ ("Main", "Base", "Core") && return ""
loc = Base.locate_package(id)
loc === nothing && return ""
return dirname(dirname(loc))
end
31 changes: 30 additions & 1 deletion test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,35 @@
using CodeTracking
using Test
# Note: ColorTypes needs to be installed, but note the absence of `using`

include("script.jl")

@testset "CodeTracking.jl" begin
# Write your own tests here.
m = first(methods(f1))
file, line = whereis(m)
@test file == normpath(joinpath(@__DIR__, "script.jl"))
src = definition(m, String)
@test src == """
function f1(x, y)
return x + y
end
"""

m = first(methods(f2))
src = definition(m, String)
@test src == """
f2(x, y) = x + y
"""

info = PkgFiles(Base.PkgId(CodeTracking))
@test Base.PkgId(info) === info.id
@test CodeTracking.basedir(info) == dirname(@__DIR__)

io = IOBuffer()
show(io, info)
str = String(take!(io))
@test startswith(str, "PkgFiles(CodeTracking [da1fd8a2-8d9e-5ec2-8556-3022fb5608a2]):\n basedir:")

@test pkgfiles("ColorTypes") === nothing
@test_throws ErrorException pkgfiles("NotAPkg")
end
5 changes: 5 additions & 0 deletions test/script.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function f1(x, y)
return x + y
end

f2(x, y) = x + y