From 015563fc6747b634dde19fa473a5da1556800562 Mon Sep 17 00:00:00 2001 From: aviatesk Date: Tue, 29 Oct 2019 23:37:45 +0900 Subject: [PATCH 1/5] implement rename refactor --- src/Atom.jl | 1 + src/refactor.jl | 248 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 249 insertions(+) create mode 100644 src/refactor.jl diff --git a/src/Atom.jl b/src/Atom.jl index 7b4f15cb..5ef85c22 100644 --- a/src/Atom.jl +++ b/src/Atom.jl @@ -57,6 +57,7 @@ include("completions.jl") include("goto.jl") include("datatip.jl") include("formatter.jl") +include("refactor.jl") include("frontend.jl") include("debugger/debugger.jl") include("profiler/profiler.jl") diff --git a/src/refactor.jl b/src/refactor.jl new file mode 100644 index 00000000..6c675290 --- /dev/null +++ b/src/refactor.jl @@ -0,0 +1,248 @@ +handle("renamerefactor") do data + @destruct [ + old, + full, + new, + # local context + column || 1, + row || 1, + startRow || 0, + context || "", + # module context + mod || "Main", + ] = data + renamerefactor(old, full, new, column, row, startRow, context, mod) +end + +# NOTE: invalid identifiers will be caught by frontend +function renamerefactor( + old, full, new, + column = 1, row = 1, startrow = 0, context = "", + mod = "Main", +) + # catch keyword renaming + iskeyword(old) && return Dict(:warning => "Keywords can't be renamed: `$old`") + + mod = getmodule(mod) + hstr = first(split(full, '.')) + head = getfield′(mod, hstr) + + # catch field renaming + hstr ≠ old && !isa(head, Module) && return Dict( + :warning => "Rename refactoring on a field isn't available: `$hstr.$old`" + ) + + expr = CSTParser.parse(context) + bind = let + if expr !== nothing + items = toplevelitems(expr, context) + ind = findfirst(item -> item isa ToplevelBinding, items) + ind === nothing ? nothing : items[ind].bind + else + nothing + end + end + + # local rename refactor if `old` isn't a toplevel binding + if islocalrefactor(bind, old) + try + refactored = localrenamerefactor(old, new, column, row, startrow, context, expr) + return isempty(refactored) ? + # NOTE: global refactoring not on definition, e.g.: on a call site, will be caught here + Dict(:info => contextdescription(old, mod, context)) : + Dict( + :text => refactored, + :success => "_Local_ rename refactoring `$old` ⟹ `$new` succeeded" + ) + catch err + return Dict(:error => errdescription(old, new, err)) + end + end + + # global rename refactor if the local rename refactor didn't happen + try + kind, desc = globalrenamerefactor(old, new, mod, expr) + + # make description + if kind === :success + val = getfield′(mod, full) + moddesc = if (head isa Module && head ≠ mod) || + (applicable(parentmodule, val) && (head = parentmodule(val)) ≠ mod) + moduledescription(old, head) + else + "" + end + + desc = join(("_Global_ rename refactoring `$mod.$old` ⟹ `$mod.$new` succeeded.", moddesc, desc), "\n\n") + end + + return Dict(kind => desc) + catch err + return Dict(:error => errdescription(old, new, err)) + end +end + +islocalrefactor(bind, name) = bind === nothing || name ≠ bind.name + +# local refactor +# -------------- + +function localrenamerefactor(old, new, column, row, startrow, context, expr) + bindings = localbindings(expr, context) + line = row - startrow + scope = currentscope(old, bindings, byteoffset(context, line, column)) + scope === nothing && return "" + + currentcontext = scope.bindstr + oldsym = Symbol(old) + newsym = Symbol(new) + newcontext = MacroTools.textwalk(currentcontext) do sym + sym === oldsym ? newsym : sym + end + + replace(context, currentcontext => newcontext) +end +localrenamerefactor(old, new, column, row, startrow, context, expr::Nothing) = "" + +function currentscope(name, bindings, byteoffset) + for binding in bindings + isa(binding, LocalScope) || continue + + # first looks for innermost scope + childscope = currentscope(name, binding.children, byteoffset) + childscope !== nothing && return childscope + + if byteoffset in binding.span && + any(bind -> bind isa LocalBinding && name == bind.name, binding.children) + return binding + end + end + + return nothing +end + +# global refactor +# --------------- + +function globalrenamerefactor(old, new, mod, expr) + entrypath, _ = if mod == Main + MAIN_MODULE_LOCATION[] + else + moduledefinition(mod) + end + + files = modulefiles(entrypath) + + # catch refactorings on an unsaved / non-existing file + isempty(files) && return :warning, unsaveddescription() + + # catch refactorings on files without write permission + nonwritables = nonwritablefiles(files) + if !isempty(nonwritables) + return :warning, nonwritablesdescription(mod, nonwritables) + end + + with_logger(JunoProgressLogger()) do + _globalrenamerefactor(old, new, mod, expr, files) + end +end + +function _globalrenamerefactor(old, new, mod, expr, files) + ismacro = CSTParser.defines_macro(expr) + oldsym = ismacro ? Symbol("@" * old) : Symbol(old) + newsym = ismacro ? Symbol("@" * new) : Symbol(new) + + total = length(files) + # TODO: enable line location information (the upstream needs to be enhanced) + modifiedfiles = Set{String}() + + id = "global_rename_refactor_progress" + @info "Start global rename refactoring" progress=0 _id=id + + for (i, file) ∈ enumerate(files) + @logmsg -1 "Refactoring: $file ($i / $total)" progress=i/total _id=id + + MacroTools.sourcewalk(file) do ex + if ex === oldsym + push!(modifiedfiles, fullpath(file)) + newsym + # handle dot accessor + elseif @capture(ex, m_.$oldsym) && getfield′(mod, Symbol(m)) isa Module + push!(modifiedfiles, fullpath(file)) + Expr(:., m, newsym) + # macro case + elseif ismacro && @capture(ex, macro $(Symbol(old))(args__) body_ end) + push!(modifiedfiles, fullpath(file)) + Expr(:macro, :($(Symbol(new))($(args...))), :($body)) + else + ex + end + end + end + + @info "Finish global rename refactoring" progress=1 _id=id + + return if !isempty(modifiedfiles) + :success, filesdescription(mod, modifiedfiles) + else + :warning, "No rename refactoring occured on `$old` in `$mod` module." + end +end + +# descriptions +# ------------ + +function contextdescription(old, mod, context) + gotouri = urigoto(mod, old) + """ + `$old` isn't found in local bindings in the current context: +
Context:
$(strip(context))

+ + If you want a global rename refactoring on `$mod.$old`, you need to run this command + from its definition. + """ +end + +function moduledescription(old, parentmod) + gotouri = urigoto(parentmod, old) + """ + **NOTE**: `$old` is defined in `$parentmod` -- you may need the same rename refactorings + in that module as well. + """ +end + +function unsaveddescription() + """ + Global rename refactor failed, since the given file isn't saved on the disk yet. + Please run this command again after you save the file. + """ +end + +function nonwritablesdescription(mod, files) + filelist = join(("
  • [$file]($(uriopen(file)))
  • " for file in files), '\n') + """ + Global rename refactor failed, since there are non-writable files detected in + `$mod` module. Please make sure the files have an write access. + +
    + Non writable files (all in `$mod` module): +
    + """ +end + +function filesdescription(mod, files) + filelist = join(("
  • [$file]($(uriopen(file)))
  • " for file in files), '\n') + """ +
    + Refactored files (all in `$mod` module): +
    + """ +end + +function errdescription(old, new, err) + """ + Rename refactoring `$old` ⟹ `$new` failed. + +
    Error:
    $(errmsg(err))

    + """ +end From a8dd150ca7a6501a434aa3dca4f1a7c0f27489d1 Mon Sep 17 00:00:00 2001 From: aviatesk Date: Sat, 9 Nov 2019 18:22:02 +0900 Subject: [PATCH 2/5] update to #215 --- src/refactor.jl | 81 ++++++++++++++++++++++++------------------------- 1 file changed, 39 insertions(+), 42 deletions(-) diff --git a/src/refactor.jl b/src/refactor.jl index 6c675290..40c4384a 100644 --- a/src/refactor.jl +++ b/src/refactor.jl @@ -1,8 +1,8 @@ handle("renamerefactor") do data @destruct [ - old, - full, - new, + oldWord, + fullWord, + newWord, # local context column || 1, row || 1, @@ -11,74 +11,71 @@ handle("renamerefactor") do data # module context mod || "Main", ] = data - renamerefactor(old, full, new, column, row, startRow, context, mod) + renamerefactor(oldWord, fullWord, newWord, column, row, startRow, context, mod) end # NOTE: invalid identifiers will be caught by frontend function renamerefactor( - old, full, new, + oldword, fullword, newword, column = 1, row = 1, startrow = 0, context = "", mod = "Main", ) # catch keyword renaming - iskeyword(old) && return Dict(:warning => "Keywords can't be renamed: `$old`") + iskeyword(oldword) && return Dict(:warning => "Keywords can't be renamed: `$oldword`") mod = getmodule(mod) - hstr = first(split(full, '.')) + hstr = first(split(fullword, '.')) head = getfield′(mod, hstr) # catch field renaming - hstr ≠ old && !isa(head, Module) && return Dict( - :warning => "Rename refactoring on a field isn't available: `$hstr.$old`" + hstr ≠ oldword && !isa(head, Module) && return Dict( + :warning => "Rename refactoring on a field isn't available: `$hstr.$oldword`" ) expr = CSTParser.parse(context) + bind = let - if expr !== nothing - items = toplevelitems(expr, context) - ind = findfirst(item -> item isa ToplevelBinding, items) - ind === nothing ? nothing : items[ind].bind - else - nothing - end + items = toplevelitems(context, expr) + ind = findfirst(item -> item isa ToplevelBinding, items) + ind === nothing ? nothing : items[ind].bind end # local rename refactor if `old` isn't a toplevel binding - if islocalrefactor(bind, old) + if islocalrefactor(bind, oldword) try - refactored = localrenamerefactor(old, new, column, row, startrow, context, expr) + refactored = localrenamerefactor(oldword, newword, column, row, startrow, context, expr) return isempty(refactored) ? # NOTE: global refactoring not on definition, e.g.: on a call site, will be caught here - Dict(:info => contextdescription(old, mod, context)) : + Dict(:info => contextdescription(oldword, mod, context)) : Dict( :text => refactored, - :success => "_Local_ rename refactoring `$old` ⟹ `$new` succeeded" + :success => "_Local_ rename refactoring `$oldword` ⟹ `$newword` succeeded" ) catch err - return Dict(:error => errdescription(old, new, err)) + return Dict(:error => errdescription(oldword, newword, err)) end end # global rename refactor if the local rename refactor didn't happen try - kind, desc = globalrenamerefactor(old, new, mod, expr) + kind, desc = globalrenamerefactor(oldword, newword, mod, expr) # make description if kind === :success - val = getfield′(mod, full) + val = getfield′(mod, fullword) moddesc = if (head isa Module && head ≠ mod) || (applicable(parentmodule, val) && (head = parentmodule(val)) ≠ mod) - moduledescription(old, head) + moduledescription(oldword, head) else "" end - desc = join(("_Global_ rename refactoring `$mod.$old` ⟹ `$mod.$new` succeeded.", moddesc, desc), "\n\n") + desc = join(("_Global_ rename refactoring `$mod.$oldword` ⟹ `$mod.$newword` succeeded.", moddesc, desc), "\n\n") end return Dict(kind => desc) catch err - return Dict(:error => errdescription(old, new, err)) + return Dict(:error => errdescription(oldword, newword, err)) end end @@ -87,22 +84,22 @@ islocalrefactor(bind, name) = bind === nothing || name ≠ bind.name # local refactor # -------------- -function localrenamerefactor(old, new, column, row, startrow, context, expr) +function localrenamerefactor(oldword, newword, column, row, startrow, context, expr) bindings = localbindings(expr, context) line = row - startrow - scope = currentscope(old, bindings, byteoffset(context, line, column)) + scope = currentscope(oldword, bindings, byteoffset(context, line, column)) scope === nothing && return "" currentcontext = scope.bindstr - oldsym = Symbol(old) - newsym = Symbol(new) newcontext = MacroTools.textwalk(currentcontext) do sym + oldsym = Symbol(oldword) + newsym = Symbol(newword) sym === oldsym ? newsym : sym end replace(context, currentcontext => newcontext) end -localrenamerefactor(old, new, column, row, startrow, context, expr::Nothing) = "" +localrenamerefactor(oldword, newword, column, row, startrow, context, expr::Nothing) = "" function currentscope(name, bindings, byteoffset) for binding in bindings @@ -124,14 +121,14 @@ end # global refactor # --------------- -function globalrenamerefactor(old, new, mod, expr) +function globalrenamerefactor(oldword, newword, mod, expr) entrypath, _ = if mod == Main MAIN_MODULE_LOCATION[] else moduledefinition(mod) end - files = modulefiles(entrypath) + files = modulefiles(string(mod), entrypath) # catch refactorings on an unsaved / non-existing file isempty(files) && return :warning, unsaveddescription() @@ -143,14 +140,14 @@ function globalrenamerefactor(old, new, mod, expr) end with_logger(JunoProgressLogger()) do - _globalrenamerefactor(old, new, mod, expr, files) + _globalrenamerefactor(oldword, newword, mod, expr, files) end end -function _globalrenamerefactor(old, new, mod, expr, files) +function _globalrenamerefactor(oldword, newword, mod, expr, files) ismacro = CSTParser.defines_macro(expr) - oldsym = ismacro ? Symbol("@" * old) : Symbol(old) - newsym = ismacro ? Symbol("@" * new) : Symbol(new) + oldsym = ismacro ? Symbol("@" * oldword) : Symbol(oldword) + newsym = ismacro ? Symbol("@" * newword) : Symbol(newword) total = length(files) # TODO: enable line location information (the upstream needs to be enhanced) @@ -171,9 +168,9 @@ function _globalrenamerefactor(old, new, mod, expr, files) push!(modifiedfiles, fullpath(file)) Expr(:., m, newsym) # macro case - elseif ismacro && @capture(ex, macro $(Symbol(old))(args__) body_ end) + elseif ismacro && @capture(ex, macro $(Symbol(oldword))(args__) body_ end) push!(modifiedfiles, fullpath(file)) - Expr(:macro, :($(Symbol(new))($(args...))), :($body)) + Expr(:macro, :($(Symbol(newword))($(args...))), :($body)) else ex end @@ -185,7 +182,7 @@ function _globalrenamerefactor(old, new, mod, expr, files) return if !isempty(modifiedfiles) :success, filesdescription(mod, modifiedfiles) else - :warning, "No rename refactoring occured on `$old` in `$mod` module." + :warning, "No rename refactoring occured on `$oldword` in `$mod` module." end end @@ -239,9 +236,9 @@ function filesdescription(mod, files) """ end -function errdescription(old, new, err) +function errdescription(oldword, newword, err) """ - Rename refactoring `$old` ⟹ `$new` failed. + Rename refactoring `$oldword` ⟹ `$newword` failed.
    Error:
    $(errmsg(err))

    """ From 814d275460e9693bd4caf24a98c426bea479224b Mon Sep 17 00:00:00 2001 From: aviatesk Date: Sat, 9 Nov 2019 18:23:21 +0900 Subject: [PATCH 3/5] update to https://github.com/MikeInnes/MacroTools.jl/pull/127 --- Project.toml | 3 ++- src/debugger/stepper.jl | 1 - src/refactor.jl | 6 ++++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Project.toml b/Project.toml index 99090cfa..66ac187d 100644 --- a/Project.toml +++ b/Project.toml @@ -28,6 +28,7 @@ Profile = "9abbd945-dff8-562f-b5e8-e1ebf5ef1b79" REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" Requires = "ae029012-a4dd-5104-9daa-d747884805df" Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" +SourceWalk = "2987fe4f-acca-4bd1-87ee-98b3d562f1a1" Traceur = "37b6cedf-1f77-55f8-9503-c64b63398394" TreeViews = "a2a6695c-b41b-5b7d-aed9-dbfdeacea5d7" WebIO = "0f1e0344-ec1d-5b48-a673-e5cf874b6c29" @@ -47,7 +48,7 @@ JuliaInterpreter = "^0.7.10" Juno = "^0.7, ^0.8" LNR = "^0.2.0" Lazy = "^0.13.2, ^0.14, ^0.15" -MacroTools = "^0.5" +MacroTools = "^0.5.1" Media = "^0.5" OrderedCollections = "^1.1" Requires = "^0.5, 1.0" diff --git a/src/debugger/stepper.jl b/src/debugger/stepper.jl index 3c596eb1..c3e423e7 100644 --- a/src/debugger/stepper.jl +++ b/src/debugger/stepper.jl @@ -3,7 +3,6 @@ using JuliaInterpreter: pc_expr, extract_args, debug_command, root, caller, import JuliaInterpreter import ..Atom: fullpath, handle, @msg, Inline, display_error, hideprompt, getmodule import Juno: Row -using MacroTools mutable struct DebuggerState frame::Union{Nothing, Frame} diff --git a/src/refactor.jl b/src/refactor.jl index 40c4384a..a5c987f9 100644 --- a/src/refactor.jl +++ b/src/refactor.jl @@ -1,3 +1,5 @@ +using SourceWalk: textwalk, sourcewalk + handle("renamerefactor") do data @destruct [ oldWord, @@ -91,9 +93,9 @@ function localrenamerefactor(oldword, newword, column, row, startrow, context, e scope === nothing && return "" currentcontext = scope.bindstr - newcontext = MacroTools.textwalk(currentcontext) do sym oldsym = Symbol(oldword) newsym = Symbol(newword) + newcontext = textwalk(currentcontext) do sym sym === oldsym ? newsym : sym end @@ -159,7 +161,7 @@ function _globalrenamerefactor(oldword, newword, mod, expr, files) for (i, file) ∈ enumerate(files) @logmsg -1 "Refactoring: $file ($i / $total)" progress=i/total _id=id - MacroTools.sourcewalk(file) do ex + sourcewalk(file) do ex if ex === oldsym push!(modifiedfiles, fullpath(file)) newsym From 8b12823a1fbad95b5204a121d6030a1fdd4b260c Mon Sep 17 00:00:00 2001 From: aviatesk Date: Sun, 24 Nov 2019 22:36:01 +0900 Subject: [PATCH 4/5] fix html tags --- src/refactor.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/refactor.jl b/src/refactor.jl index a5c987f9..692c0ddc 100644 --- a/src/refactor.jl +++ b/src/refactor.jl @@ -195,7 +195,7 @@ function contextdescription(old, mod, context) gotouri = urigoto(mod, old) """ `$old` isn't found in local bindings in the current context: -
    Context:
    $(strip(context))

    +
    Context:
    $(strip(context))
    If you want a global rename refactoring on `$mod.$old`, you need to run this command from its definition. @@ -242,6 +242,6 @@ function errdescription(oldword, newword, err) """ Rename refactoring `$oldword` ⟹ `$newword` failed. -
    Error:
    $(errmsg(err))

    +
    Error:
    $(errmsg(err))
    """ end From 8c5f32d2842cd9d86a38e646c07b8544751b43d3 Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki Date: Mon, 10 Feb 2020 06:05:49 +0900 Subject: [PATCH 5/5] wip: test --- test/refactor.jl | 63 ++++++++++++++++++++++++++++++++++++++++++++++++ test/runtests.jl | 1 + 2 files changed, 64 insertions(+) create mode 100644 test/refactor.jl diff --git a/test/refactor.jl b/test/refactor.jl new file mode 100644 index 00000000..2b8fb1b6 --- /dev/null +++ b/test/refactor.jl @@ -0,0 +1,63 @@ +@testset "rename refactor" begin + import Atom: renamerefactor + + @testset "catch invalid/unsupported refactorings" begin + # catch renaming on keywords + let result = renamerefactor("function", "function", "func") + @test haskey(result, :warning) + end + + # catch field renaming + let result = renamerefactor("field", "obj.field", "newfield") + @test haskey(result, :warning) + end + + # but when dot-accessed object is a (existing) module, continues refactoring + let result = renamerefactor("bind", "Main.bind", "newbind") + @test !haskey(result, :warning) + @test haskey(result, :info) + end + end + + @testset "local rename refactor" begin + # TODO + end + + @testset "global rename refactor" begin + @testset "catch edge cases" begin + # handle refactoring on an unsaved / non-existing file + let context = """ + toplevel() = nothing + """ + # mock MAIN_MODULE_LOCATION update in `module` handler + @eval Atom begin + MAIN_MODULE_LOCATION[] = "", 1 + end + result = renamerefactor("toplevel", "toplevel", "toplevel2", 0, 1, 1, context) + @test haskey(result, :warning) + @test result[:warning] == Atom.unsaveddescription() + end + + # handle refactoring on nonwritable files + let path = joinpath(@__DIR__, "fixtures", "Junk.jl") + originalmode = Base.filemode(path) + + try + Base.chmod(path, 0x444) # only reading + context = "module Junk2 end" + result = renamerefactor("Junk2", "Junk2", "Junk3", 0, 1, 1, context, "Main.Junk") + + @test haskey(result, :warning) + @test result[:warning] == Atom.nonwritablesdescription(Main.Junk, [path]) + catch err + @info """ + Cancelled the test for handling of refactorings on non-writable + files due to the error below: + """ err + finally + Base.chmod(path, originalmode) + end + end + end + end +end diff --git a/test/runtests.jl b/test/runtests.jl index 8ce27151..b8c5afc0 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -58,5 +58,6 @@ include("outline.jl") include("completions.jl") include("goto.jl") include("datatip.jl") +include("refactor.jl") include("workspace.jl") include("docs.jl")