mvz-live_ast 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGES.rdoc +93 -0
- data/README.rdoc +419 -0
- data/Rakefile +21 -0
- data/devel/levitate.rb +853 -0
- data/devel/levitate_config.rb +4 -0
- data/lib/live_ast.rb +4 -0
- data/lib/live_ast/ast_eval.rb +11 -0
- data/lib/live_ast/ast_load.rb +15 -0
- data/lib/live_ast/base.rb +73 -0
- data/lib/live_ast/common.rb +48 -0
- data/lib/live_ast/error.rb +20 -0
- data/lib/live_ast/evaler.rb +32 -0
- data/lib/live_ast/full.rb +2 -0
- data/lib/live_ast/irb_spy.rb +43 -0
- data/lib/live_ast/linker.rb +122 -0
- data/lib/live_ast/loader.rb +60 -0
- data/lib/live_ast/reader.rb +26 -0
- data/lib/live_ast/replace_eval.rb +121 -0
- data/lib/live_ast/replace_load.rb +14 -0
- data/lib/live_ast/replace_raise.rb +18 -0
- data/lib/live_ast/ruby_parser.rb +36 -0
- data/lib/live_ast/ruby_parser/test.rb +197 -0
- data/lib/live_ast/ruby_parser/unparser.rb +13 -0
- data/lib/live_ast/to_ast.rb +26 -0
- data/lib/live_ast/to_ruby.rb +24 -0
- data/lib/live_ast/version.rb +3 -0
- data/test/ast_eval_feature_test.rb +11 -0
- data/test/ast_load_feature_test.rb +11 -0
- data/test/attr_test.rb +24 -0
- data/test/backtrace_test.rb +158 -0
- data/test/covert_define_method_test.rb +23 -0
- data/test/def_test.rb +35 -0
- data/test/define_method_test.rb +67 -0
- data/test/define_singleton_method_test.rb +15 -0
- data/test/encoding_test.rb +52 -0
- data/test/encoding_test/bad.rb +1 -0
- data/test/encoding_test/cp932.rb +6 -0
- data/test/encoding_test/default.rb +5 -0
- data/test/encoding_test/eucjp.rb +6 -0
- data/test/encoding_test/koi8.rb +6 -0
- data/test/encoding_test/koi8_shebang.rb +7 -0
- data/test/encoding_test/koi8_with_utf8bom.rb +6 -0
- data/test/encoding_test/usascii.rb +6 -0
- data/test/encoding_test/usascii_with_utf8bom.rb +6 -0
- data/test/encoding_test/utf8.rb +6 -0
- data/test/encoding_test/utf8bom.rb +6 -0
- data/test/encoding_test/utf8bom_only.rb +5 -0
- data/test/encoding_test/utf8dos.rb +6 -0
- data/test/encoding_test/utf8mac.rb +6 -0
- data/test/encoding_test/utf8mac_alt.rb +6 -0
- data/test/encoding_test/utf8unix.rb +6 -0
- data/test/error_test.rb +116 -0
- data/test/eval_test.rb +269 -0
- data/test/flush_cache_test.rb +98 -0
- data/test/irb_test.rb +25 -0
- data/test/lambda_test.rb +56 -0
- data/test/load_path_test.rb +78 -0
- data/test/load_test.rb +123 -0
- data/test/main.rb +140 -0
- data/test/nested_test.rb +29 -0
- data/test/noninvasive_test.rb +51 -0
- data/test/readme_test.rb +16 -0
- data/test/recursive_eval_test.rb +52 -0
- data/test/redefine_method_test.rb +83 -0
- data/test/reload_test.rb +105 -0
- data/test/replace_eval_test.rb +405 -0
- data/test/rubygems_test.rb +25 -0
- data/test/rubyspec_test.rb +39 -0
- data/test/singleton_test.rb +25 -0
- data/test/stdlib_test.rb +13 -0
- data/test/thread_test.rb +44 -0
- data/test/to_ast_feature_test.rb +15 -0
- data/test/to_ruby_feature_test.rb +15 -0
- data/test/to_ruby_test.rb +87 -0
- metadata +275 -0
data/lib/live_ast.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'live_ast/base'
|
2
|
+
|
3
|
+
module Kernel
|
4
|
+
private
|
5
|
+
|
6
|
+
#
|
7
|
+
# For use in noninvasive mode (<code>require 'live_ast/base'</code>).
|
8
|
+
#
|
9
|
+
# Same behavior as the built-in +load+ except that AST-accessible
|
10
|
+
# objects are created.
|
11
|
+
#
|
12
|
+
def ast_load(file, wrap = false)
|
13
|
+
LiveAST::Loader.load(file, wrap)
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
require 'live_ast/common'
|
4
|
+
require 'live_ast/reader'
|
5
|
+
require 'live_ast/evaler'
|
6
|
+
require 'live_ast/linker'
|
7
|
+
require 'live_ast/loader'
|
8
|
+
require 'live_ast/error'
|
9
|
+
require 'live_ast/irb_spy' if defined?(IRB)
|
10
|
+
|
11
|
+
module LiveAST
|
12
|
+
NATIVE_EVAL = Kernel.method(:eval) #:nodoc:
|
13
|
+
|
14
|
+
class << self
|
15
|
+
attr_writer :parser #:nodoc:
|
16
|
+
|
17
|
+
def parser #:nodoc:
|
18
|
+
@parser ||= (
|
19
|
+
require 'live_ast/ruby_parser'
|
20
|
+
LiveAST::RubyParser
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
#
|
25
|
+
# For use in noninvasive mode (<code>require 'live_ast/base'</code>).
|
26
|
+
#
|
27
|
+
# Equivalent to <code>obj.to_ast</code>.
|
28
|
+
#
|
29
|
+
def ast(obj) #:nodoc:
|
30
|
+
case obj
|
31
|
+
when Method, UnboundMethod
|
32
|
+
Linker.find_method_ast(obj.owner, obj.name, *obj.source_location)
|
33
|
+
when Proc
|
34
|
+
Linker.find_proc_ast(obj)
|
35
|
+
else
|
36
|
+
raise TypeError, "No AST for #{obj.class} objects"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
#
|
41
|
+
# Flush unused ASTs from the cache. See README.rdoc before doing
|
42
|
+
# this.
|
43
|
+
#
|
44
|
+
def flush_cache
|
45
|
+
Linker.flush_cache
|
46
|
+
end
|
47
|
+
|
48
|
+
#
|
49
|
+
# For use in noninvasive mode (<code>require 'live_ast/base'</code>).
|
50
|
+
#
|
51
|
+
# Equivalent to <code>Kernel#ast_eval</code>.
|
52
|
+
#
|
53
|
+
def eval(*args) #:nodoc:
|
54
|
+
Evaler.eval(args[0], *args)
|
55
|
+
end
|
56
|
+
|
57
|
+
#
|
58
|
+
# For use in noninvasive mode (<code>require 'live_ast/base'</code>).
|
59
|
+
#
|
60
|
+
# Equivalent to <code>Kernel#ast_load</code>.
|
61
|
+
#
|
62
|
+
def load(file, wrap = false) #:nodoc:
|
63
|
+
Loader.load(file, wrap)
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
# strip the revision token from a string
|
68
|
+
#
|
69
|
+
def strip_token(file) #:nodoc:
|
70
|
+
file.sub(/#{Regexp.quote Linker::REVISION_TOKEN}[a-z]+/, "")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
|
2
|
+
module LiveAST
|
3
|
+
module Common
|
4
|
+
module_function
|
5
|
+
|
6
|
+
def arg_to_str(arg)
|
7
|
+
begin
|
8
|
+
arg.to_str
|
9
|
+
rescue NameError
|
10
|
+
thing = if arg.nil? then nil else arg.class end
|
11
|
+
|
12
|
+
raise TypeError, "can't convert #{thing.inspect} into String"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def check_arity(args, range)
|
17
|
+
unless range.include? args.size
|
18
|
+
range = 0 if range == (0..0)
|
19
|
+
|
20
|
+
raise ArgumentError,
|
21
|
+
"wrong number of arguments (#{args.size} for #{range})"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def check_type(obj, klass)
|
26
|
+
unless obj.is_a? klass
|
27
|
+
raise TypeError, "wrong argument type #{obj.class} (expected #{klass})"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def location_for_eval(*args)
|
32
|
+
bind, *location = args
|
33
|
+
|
34
|
+
if bind
|
35
|
+
case location.size
|
36
|
+
when 0
|
37
|
+
NATIVE_EVAL.call("[__FILE__, __LINE__]", bind)
|
38
|
+
when 1
|
39
|
+
[location.first, 1]
|
40
|
+
else
|
41
|
+
location
|
42
|
+
end
|
43
|
+
else
|
44
|
+
["(eval)", 1]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module LiveAST
|
2
|
+
class MultipleDefinitionsOnSameLineError < ScriptError
|
3
|
+
def message
|
4
|
+
"AST requested for a method or block that shares a line " <<
|
5
|
+
"with another method or block."
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class ASTNotFoundError < StandardError
|
10
|
+
def message
|
11
|
+
"The requested AST could not be found (AST flushed or compiled code)."
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class RawEvalError < ASTNotFoundError
|
16
|
+
def message
|
17
|
+
"Must use ast_eval instead of eval in order to obtain AST."
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module LiveAST
|
2
|
+
module Evaler
|
3
|
+
class << self
|
4
|
+
include Common
|
5
|
+
|
6
|
+
def eval(parser_source, *args)
|
7
|
+
evaler_source, bind, *rest = handle_args(*args)
|
8
|
+
|
9
|
+
file, line = location_for_eval(bind, *rest)
|
10
|
+
file = LiveAST.strip_token(file)
|
11
|
+
|
12
|
+
key, _ = Linker.new_cache_synced(parser_source, file, line, false)
|
13
|
+
|
14
|
+
begin
|
15
|
+
NATIVE_EVAL.call(evaler_source, bind, key, line)
|
16
|
+
rescue Exception => ex
|
17
|
+
ex.backtrace.map! { |s| LiveAST.strip_token s }
|
18
|
+
raise ex
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def handle_args(*args)
|
23
|
+
args.tap do
|
24
|
+
check_arity(args, 2..4)
|
25
|
+
args[0] = arg_to_str(args[0])
|
26
|
+
check_type(args[1], Binding)
|
27
|
+
args[2] = arg_to_str(args[2]) if args[2]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
|
2
|
+
module LiveAST
|
3
|
+
@history = nil
|
4
|
+
|
5
|
+
module IRBSpy
|
6
|
+
class << self
|
7
|
+
attr_writer :history
|
8
|
+
|
9
|
+
def code_at(line)
|
10
|
+
unless @history
|
11
|
+
raise NotImplementedError,
|
12
|
+
"LiveAST cannot access history for this IRB input method"
|
13
|
+
end
|
14
|
+
grow = 0
|
15
|
+
begin
|
16
|
+
code = @history[line..(line + grow)].join
|
17
|
+
LiveAST.parser.new.parse(code) or raise "#{LiveAST.parser} error"
|
18
|
+
rescue
|
19
|
+
grow += 1
|
20
|
+
retry if line + grow < @history.size
|
21
|
+
raise
|
22
|
+
end
|
23
|
+
code
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
[
|
30
|
+
defined?(IRB::StdioInputMethod) ? IRB::StdioInputMethod : nil,
|
31
|
+
defined?(IRB::ReadlineInputMethod) ? IRB::ReadlineInputMethod : nil,
|
32
|
+
].compact.each do |klass|
|
33
|
+
klass.module_eval do
|
34
|
+
alias_method :live_ast_original_gets, :gets
|
35
|
+
def gets
|
36
|
+
live_ast_original_gets.tap do
|
37
|
+
if defined?(@line)
|
38
|
+
LiveAST::IRBSpy.history = @line
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module LiveAST
|
2
|
+
class Cache
|
3
|
+
def initialize(*args)
|
4
|
+
@source, @user_line = args
|
5
|
+
@asts = nil
|
6
|
+
end
|
7
|
+
|
8
|
+
def fetch_ast(line)
|
9
|
+
@asts ||= LiveAST.parser.new.parse(@source).tap do
|
10
|
+
@source = nil
|
11
|
+
end
|
12
|
+
@asts.delete(line - @user_line + 1)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module Attacher
|
17
|
+
VAR_NAME = :@_live_ast
|
18
|
+
|
19
|
+
def attach_to_proc(obj, ast)
|
20
|
+
obj.instance_variable_set(VAR_NAME, ast)
|
21
|
+
end
|
22
|
+
|
23
|
+
def fetch_proc_attachment(obj)
|
24
|
+
if obj.instance_variable_defined?(VAR_NAME)
|
25
|
+
obj.instance_variable_get(VAR_NAME)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def attach_to_method(klass, method, ast)
|
30
|
+
unless klass.instance_variable_defined?(VAR_NAME)
|
31
|
+
klass.instance_variable_set(VAR_NAME, {})
|
32
|
+
end
|
33
|
+
klass.instance_variable_get(VAR_NAME)[method] = ast
|
34
|
+
end
|
35
|
+
|
36
|
+
def fetch_method_attachment(klass, method)
|
37
|
+
if klass.instance_variable_defined?(VAR_NAME)
|
38
|
+
klass.instance_variable_get(VAR_NAME)[method]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
module Linker
|
44
|
+
REVISION_TOKEN = "|ast@"
|
45
|
+
|
46
|
+
@caches = {}
|
47
|
+
@counter = "a"
|
48
|
+
@mutex = Mutex.new
|
49
|
+
|
50
|
+
class << self
|
51
|
+
include Attacher
|
52
|
+
|
53
|
+
def find_proc_ast(obj)
|
54
|
+
@mutex.synchronize do
|
55
|
+
fetch_proc_attachment(obj) or (
|
56
|
+
ast = find_ast(*obj.source_location) or raise ASTNotFoundError
|
57
|
+
attach_to_proc(obj, ast)
|
58
|
+
)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def find_method_ast(klass, name, *location)
|
63
|
+
@mutex.synchronize do
|
64
|
+
case ast = find_ast(*location)
|
65
|
+
when nil
|
66
|
+
fetch_method_attachment(klass, name) or raise ASTNotFoundError
|
67
|
+
else
|
68
|
+
attach_to_method(klass, name, ast)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def find_ast(*location)
|
74
|
+
raise ASTNotFoundError unless location.size == 2
|
75
|
+
raise RawEvalError if location.first == "(eval)"
|
76
|
+
ast = fetch_from_cache(*location)
|
77
|
+
raise MultipleDefinitionsOnSameLineError if ast == :multiple
|
78
|
+
ast
|
79
|
+
end
|
80
|
+
|
81
|
+
def fetch_from_cache(file, line)
|
82
|
+
cache = @caches[file]
|
83
|
+
if !cache and !file.index(REVISION_TOKEN)
|
84
|
+
_, cache =
|
85
|
+
if defined?(IRB) and file == "(irb)"
|
86
|
+
new_cache(IRBSpy.code_at(line), file, line, false)
|
87
|
+
else
|
88
|
+
#
|
89
|
+
# File was loaded by 'require'.
|
90
|
+
# Play catch-up: assume it has not changed in the meantime.
|
91
|
+
#
|
92
|
+
new_cache(Reader.read(file), file, 1, true)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
cache.fetch_ast(line) if cache
|
96
|
+
end
|
97
|
+
|
98
|
+
#
|
99
|
+
# create a cache along with a unique key for it
|
100
|
+
#
|
101
|
+
def new_cache(contents, file, user_line, file_is_key)
|
102
|
+
key = file_is_key ? file : file + REVISION_TOKEN + @counter
|
103
|
+
cache = Cache.new(contents, user_line)
|
104
|
+
@caches[key] = cache
|
105
|
+
@counter.next!
|
106
|
+
return key, cache
|
107
|
+
end
|
108
|
+
|
109
|
+
def new_cache_synced(*args)
|
110
|
+
@mutex.synchronize do
|
111
|
+
new_cache(*args)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def flush_cache
|
116
|
+
@mutex.synchronize do
|
117
|
+
@caches.delete_if { |key, _| key.index REVISION_TOKEN }
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module LiveAST
|
2
|
+
module Loader
|
3
|
+
class << self
|
4
|
+
def load(file, wrap)
|
5
|
+
file = find_file(file)
|
6
|
+
|
7
|
+
# guards to protect toplevel locals
|
8
|
+
header, footer, warnings_ok = header_footer(wrap)
|
9
|
+
|
10
|
+
parser_src = Reader.read(file)
|
11
|
+
evaler_src = header << parser_src << footer
|
12
|
+
|
13
|
+
run = lambda do
|
14
|
+
Evaler.eval(parser_src, evaler_src, TOPLEVEL_BINDING, file, 1)
|
15
|
+
end
|
16
|
+
warnings_ok ? run.call : suppress_warnings(&run)
|
17
|
+
true
|
18
|
+
end
|
19
|
+
|
20
|
+
def header_footer(wrap)
|
21
|
+
if wrap
|
22
|
+
return "class << Object.new;", ";end", true
|
23
|
+
else
|
24
|
+
locals = NATIVE_EVAL.call("local_variables", TOPLEVEL_BINDING)
|
25
|
+
|
26
|
+
params = locals.empty? ? "" : ("|;" + locals.join(",") + "|")
|
27
|
+
|
28
|
+
return "lambda do #{params}", ";end.call", locals.empty?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def suppress_warnings
|
33
|
+
previous = $VERBOSE
|
34
|
+
$VERBOSE = nil
|
35
|
+
begin
|
36
|
+
yield
|
37
|
+
ensure
|
38
|
+
$VERBOSE ||= previous
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def find_file(file)
|
43
|
+
if file.index Linker::REVISION_TOKEN
|
44
|
+
raise "refusing to load file with revision token: `#{file}'"
|
45
|
+
end
|
46
|
+
search_paths(file) or
|
47
|
+
raise LoadError, "cannot load such file -- #{file}"
|
48
|
+
end
|
49
|
+
|
50
|
+
def search_paths(file)
|
51
|
+
return file if File.file? file
|
52
|
+
$LOAD_PATH.each do |path|
|
53
|
+
target = path + "/" + file
|
54
|
+
return target if File.file? target
|
55
|
+
end
|
56
|
+
nil
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|