From eede4fce17c3962aa0f5faa8b936b7d044ec42cf Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Thu, 21 Feb 2019 05:21:27 -0600 Subject: [PATCH 1/6] Implement `definition(method, String)` and add internal data --- src/CodeTracking.jl | 73 ++++++++++++++++++++++++++++++++++++++++++--- src/data.jl | 5 ++++ src/utils.jl | 16 ++++++++++ test/runtests.jl | 18 ++++++++++- test/script.jl | 5 ++++ 5 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 src/data.jl create mode 100644 src/utils.jl create mode 100644 test/script.jl diff --git a/src/CodeTracking.jl b/src/CodeTracking.jl index 9db2883..a1b9cde 100644 --- a/src/CodeTracking.jl +++ b/src/CodeTracking.jl @@ -1,15 +1,80 @@ module CodeTracking -export whereis +using Core: LineInfoNode -# This is just a stub implementation for now +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`. +""" +definition(method::Method, ::Type{Expr}) = get(method_definitions, method.sig, nothing) + +definition(method::Method) = definition(method, Expr) + +""" + files = pkgfiles(mod::Module) + +Return a list of the files that were loaded to define `mod`. +""" +function pkgfiles(mod::Module) + error("not implemented") +end + end # module diff --git a/src/data.jl b/src/data.jl new file mode 100644 index 0000000..6b397c5 --- /dev/null +++ b/src/data.jl @@ -0,0 +1,5 @@ +# The variables here get populated by Revise.jl. + +const method_locations = IdDict{Type,LineInfoNode}() + +const method_definitions = IdDict{Type,Expr}() diff --git a/src/utils.jl b/src/utils.jl new file mode 100644 index 0000000..93a4d18 --- /dev/null +++ b/src/utils.jl @@ -0,0 +1,16 @@ +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 diff --git a/test/runtests.jl b/test/runtests.jl index e51139f..2451bdd 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,22 @@ using CodeTracking using Test +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 + """ end diff --git a/test/script.jl b/test/script.jl new file mode 100644 index 0000000..49f08f4 --- /dev/null +++ b/test/script.jl @@ -0,0 +1,5 @@ +function f1(x, y) + return x + y +end + +f2(x, y) = x + y From 8d00f9f900d4242a6af4e8fa62561ab104814c7e Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Thu, 21 Feb 2019 07:37:01 -0600 Subject: [PATCH 2/6] Implement `pkgfiles` --- src/CodeTracking.jl | 18 +++++++++++++----- src/data.jl | 28 ++++++++++++++++++++++++++++ src/utils.jl | 7 +++++++ test/runtests.jl | 4 ++++ 4 files changed, 52 insertions(+), 5 deletions(-) diff --git a/src/CodeTracking.jl b/src/CodeTracking.jl index a1b9cde..18c902f 100644 --- a/src/CodeTracking.jl +++ b/src/CodeTracking.jl @@ -1,5 +1,6 @@ module CodeTracking +using Base: PkgId using Core: LineInfoNode export whereis, definition, pkgfiles @@ -69,12 +70,19 @@ definition(method::Method, ::Type{Expr}) = get(method_definitions, method.sig, n definition(method::Method) = definition(method, Expr) """ - files = pkgfiles(mod::Module) + info = pkgfiles(id::PkgId) -Return a list of the files that were loaded to define `mod`. +Return a [`PkgFiles`](@ref) structure with information about the files that define package `id`. +Returns `nothing` if `id` has not been loaded. """ -function pkgfiles(mod::Module) - error("not implemented") -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 diff --git a/src/data.jl b/src/data.jl index 6b397c5..3715c29 100644 --- a/src/data.jl +++ b/src/data.jl @@ -1,5 +1,33 @@ # 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 + const method_locations = IdDict{Type,LineInfoNode}() const method_definitions = IdDict{Type,Expr}() + +const _pkgfiles = Dict{PkgId,PkgFiles}() diff --git a/src/utils.jl b/src/utils.jl index 93a4d18..406efb3 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -14,3 +14,10 @@ function isfuncexpr(ex) 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 diff --git a/test/runtests.jl b/test/runtests.jl index 2451bdd..98855c4 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -19,4 +19,8 @@ include("script.jl") @test src == """ f2(x, y) = x + y """ + + info = CodeTracking.PkgFiles(Base.PkgId(CodeTracking)) + @test Base.PkgId(info) === info.id + @test CodeTracking.basedir(info) == dirname(@__DIR__) end From e8242ab89139dcd4c5894c694e9ec76876553ea1 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Thu, 21 Feb 2019 11:56:22 -0600 Subject: [PATCH 3/6] Add callback for looking up methods that have not yet populated cache --- src/CodeTracking.jl | 12 +++++++++++- src/data.jl | 2 ++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/CodeTracking.jl b/src/CodeTracking.jl index 18c902f..79b496d 100644 --- a/src/CodeTracking.jl +++ b/src/CodeTracking.jl @@ -65,7 +65,17 @@ end Return an expression that defines `method`. """ -definition(method::Method, ::Type{Expr}) = get(method_definitions, method.sig, nothing) +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) diff --git a/src/data.jl b/src/data.jl index 3715c29..763425b 100644 --- a/src/data.jl +++ b/src/data.jl @@ -31,3 +31,5 @@ const method_locations = IdDict{Type,LineInfoNode}() const method_definitions = IdDict{Type,Expr}() const _pkgfiles = Dict{PkgId,PkgFiles}() + +const method_lookup_callback = Ref{Any}(nothing) From 51dfd33a3b38d71cefb7e1e3e15377054eeae9e8 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Thu, 21 Feb 2019 11:58:15 -0600 Subject: [PATCH 4/6] Add .travis.yml --- .travis.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..403b764 --- /dev/null +++ b/.travis.yml @@ -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())' From f2d1eecd7aa99996a9d43fc1f5fff56255feee89 Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Fri, 22 Feb 2019 05:54:22 -0600 Subject: [PATCH 5/6] Add a `show` method for PkgFiles and update the README --- README.md | 38 +++++++++++++++++++++++++++++--------- src/CodeTracking.jl | 1 + src/data.jl | 7 +++++++ test/runtests.jl | 7 ++++++- 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index b668f40..45e1cf1 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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, @@ -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. diff --git a/src/CodeTracking.jl b/src/CodeTracking.jl index 79b496d..5d54dbb 100644 --- a/src/CodeTracking.jl +++ b/src/CodeTracking.jl @@ -3,6 +3,7 @@ module CodeTracking using Base: PkgId using Core: LineInfoNode +export PkgFiles export whereis, definition, pkgfiles include("data.jl") diff --git a/src/data.jl b/src/data.jl index 763425b..b9aa737 100644 --- a/src/data.jl +++ b/src/data.jl @@ -26,6 +26,13 @@ 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}() diff --git a/test/runtests.jl b/test/runtests.jl index 98855c4..9fde9b7 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -20,7 +20,12 @@ include("script.jl") f2(x, y) = x + y """ - info = CodeTracking.PkgFiles(Base.PkgId(CodeTracking)) + 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:") end From 9b9f5cd591b55af84daf8dafc97c04efb0bd239d Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Fri, 22 Feb 2019 06:03:57 -0600 Subject: [PATCH 6/6] Rework API to be around name, uuid rather than unexported Base.PkgId --- Project.toml | 6 +++++- src/CodeTracking.jl | 16 +++++++++++++--- test/runtests.jl | 4 ++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/Project.toml b/Project.toml index 9aaf89b..2814ffb 100644 --- a/Project.toml +++ b/Project.toml @@ -3,8 +3,12 @@ uuid = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2" authors = ["Tim Holy "] 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"] diff --git a/src/CodeTracking.jl b/src/CodeTracking.jl index 5d54dbb..1f117e1 100644 --- a/src/CodeTracking.jl +++ b/src/CodeTracking.jl @@ -2,6 +2,7 @@ module CodeTracking using Base: PkgId using Core: LineInfoNode +using UUIDs export PkgFiles export whereis, definition, pkgfiles @@ -81,11 +82,20 @@ end definition(method::Method) = definition(method, Expr) """ - info = pkgfiles(id::PkgId) + info = pkgfiles(name::AbstractString) + info = pkgfiles(name::AbstractString, uuid::UUID) -Return a [`PkgFiles`](@ref) structure with information about the files that define package `id`. -Returns `nothing` if `id` has not been loaded. +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") + return pkgfiles(name, uuid) +end pkgfiles(id::PkgId) = get(_pkgfiles, id, nothing) """ diff --git a/test/runtests.jl b/test/runtests.jl index 9fde9b7..25fbaab 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,5 +1,6 @@ using CodeTracking using Test +# Note: ColorTypes needs to be installed, but note the absence of `using` include("script.jl") @@ -28,4 +29,7 @@ include("script.jl") 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