mvz-live_ast 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|