live_ast 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/CHANGES.rdoc +6 -0
  2. data/MANIFEST +54 -0
  3. data/README.rdoc +388 -0
  4. data/Rakefile +19 -0
  5. data/devel/jumpstart.rb +983 -0
  6. data/lib/live_ast/ast_eval.rb +13 -0
  7. data/lib/live_ast/ast_load.rb +15 -0
  8. data/lib/live_ast/base.rb +56 -0
  9. data/lib/live_ast/cache.rb +14 -0
  10. data/lib/live_ast/error.rb +30 -0
  11. data/lib/live_ast/evaler.rb +66 -0
  12. data/lib/live_ast/linker.rb +107 -0
  13. data/lib/live_ast/loader.rb +69 -0
  14. data/lib/live_ast/parser.rb +48 -0
  15. data/lib/live_ast/replace_load.rb +14 -0
  16. data/lib/live_ast/replace_raise.rb +21 -0
  17. data/lib/live_ast/to_ast.rb +17 -0
  18. data/lib/live_ast/to_ruby.rb +12 -0
  19. data/lib/live_ast/version.rb +3 -0
  20. data/lib/live_ast.rb +4 -0
  21. data/test/ast_eval_feature_test.rb +11 -0
  22. data/test/ast_load_feature_test.rb +11 -0
  23. data/test/backtrace_test.rb +159 -0
  24. data/test/covert_define_method_test.rb +23 -0
  25. data/test/def_test.rb +35 -0
  26. data/test/define_method_test.rb +41 -0
  27. data/test/define_singleton_method_test.rb +15 -0
  28. data/test/encoding_test/bad.rb +1 -0
  29. data/test/encoding_test/cp932.rb +6 -0
  30. data/test/encoding_test/default.rb +5 -0
  31. data/test/encoding_test/eucjp.rb +6 -0
  32. data/test/encoding_test/koi8.rb +6 -0
  33. data/test/encoding_test/koi8_shebang.rb +7 -0
  34. data/test/encoding_test/usascii.rb +6 -0
  35. data/test/encoding_test/utf8.rb +6 -0
  36. data/test/encoding_test.rb +51 -0
  37. data/test/error_test.rb +115 -0
  38. data/test/eval_test.rb +269 -0
  39. data/test/flush_cache_test.rb +98 -0
  40. data/test/lambda_test.rb +56 -0
  41. data/test/load_path_test.rb +84 -0
  42. data/test/load_test.rb +85 -0
  43. data/test/noninvasive_test.rb +51 -0
  44. data/test/readme_test.rb +11 -0
  45. data/test/recursive_eval_test.rb +52 -0
  46. data/test/redefine_method_test.rb +83 -0
  47. data/test/reload_test.rb +108 -0
  48. data/test/shared/ast_generators.rb +124 -0
  49. data/test/shared/main.rb +110 -0
  50. data/test/stdlib_test.rb +11 -0
  51. data/test/thread_test.rb +44 -0
  52. data/test/to_ast_feature_test.rb +15 -0
  53. data/test/to_ruby_feature_test.rb +15 -0
  54. data/test/to_ruby_test.rb +86 -0
  55. metadata +223 -0
@@ -0,0 +1,13 @@
1
+ require 'live_ast/base'
2
+
3
+ module Kernel
4
+ private
5
+
6
+ #
7
+ # The same as +eval+ except that the binding argument is required
8
+ # and AST-accessible objects are created.
9
+ #
10
+ def ast_eval(*args)
11
+ LiveAST::Evaler.eval(args[0], *args)
12
+ end
13
+ end
@@ -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,56 @@
1
+ require 'thread'
2
+
3
+ require 'live_ast/parser'
4
+ require 'live_ast/loader'
5
+ require 'live_ast/evaler'
6
+ require 'live_ast/linker'
7
+ require 'live_ast/cache'
8
+ require 'live_ast/error'
9
+
10
+ module LiveAST
11
+ NATIVE_EVAL = Kernel.method(:eval) #:nodoc:
12
+
13
+ class << self
14
+ #
15
+ # For use in noninvasive mode (<code>require 'live_ast/base'</code>).
16
+ #
17
+ # Extract an object's AST.
18
+ #
19
+ def ast(obj) #:nodoc:
20
+ case obj
21
+ when Method, UnboundMethod
22
+ Linker.find_method_ast(obj.owner, obj.name, *obj.source_location)
23
+ when Proc
24
+ Linker.find_proc_ast(obj)
25
+ else
26
+ raise TypeError, "No AST for #{obj.class} objects"
27
+ end
28
+ end
29
+
30
+ #
31
+ # Flush unused ASTs from the cache. See README.rdoc before doing
32
+ # this.
33
+ #
34
+ def flush_cache
35
+ Linker.flush_cache
36
+ end
37
+
38
+ #
39
+ # For use in noninvasive mode (<code>require 'live_ast/base'</code>).
40
+ #
41
+ # Equivalent to Kernel#ast_eval.
42
+ #
43
+ def eval(*args) #:nodoc:
44
+ Evaler.eval(args[0], *args)
45
+ end
46
+
47
+ #
48
+ # For use in noninvasive mode (<code>require 'live_ast/base'</code>).
49
+ #
50
+ # Equivalent to Kernel#ast_load.
51
+ #
52
+ def load(file, wrap = false) #:nodoc:
53
+ Loader.load(file, wrap)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,14 @@
1
+ module LiveAST
2
+ class Cache
3
+ def initialize(*args)
4
+ @source, @user_line, @asts = args
5
+ end
6
+
7
+ def fetch_ast(line)
8
+ @asts ||= Parser.new.parse(@source).tap do
9
+ @source = nil
10
+ end
11
+ @asts.delete(line - @user_line + 1)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,30 @@
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
+ end
11
+
12
+ class RawEvalError < ASTNotFoundError
13
+ def message
14
+ "Must use ast_eval instead of eval in order to obtain AST."
15
+ end
16
+ end
17
+
18
+ class NoSourceError < ASTNotFoundError
19
+ def message
20
+ "No source found for the requested AST. " <<
21
+ "Are you sure it was written in ruby?"
22
+ end
23
+ end
24
+
25
+ class FlushedError < ASTNotFoundError
26
+ def message
27
+ "The requested AST was flushed from the cache."
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,66 @@
1
+ module LiveAST
2
+ module Evaler
3
+ class << self
4
+ def eval(parser_source, *args)
5
+ evaler_source, bind, *location = handle_args(*args)
6
+
7
+ file, line = handle_location(bind, *location)
8
+ file = Linker.strip_token(file)
9
+
10
+ key, _ = Linker.new_cache_synced(parser_source, file, line, false)
11
+
12
+ begin
13
+ NATIVE_EVAL.call(evaler_source, bind, key, line)
14
+ rescue Exception => ex
15
+ fix_backtrace(ex.backtrace)
16
+ raise ex
17
+ end
18
+ end
19
+
20
+ #
21
+ # match eval's error messages
22
+ #
23
+ def handle_args(*args)
24
+ unless (2..4).include? args.size
25
+ raise ArgumentError,
26
+ "wrong number of arguments (#{args.size} for 2..4)"
27
+ end
28
+ unless args[1].is_a? Binding
29
+ raise TypeError,
30
+ "wrong argument type #{args[1].class} (expected Binding)"
31
+ end
32
+ args[0] = arg_to_str(args[0])
33
+ args[2] = arg_to_str(args[2]) unless args[2].nil?
34
+ args
35
+ end
36
+
37
+ def arg_to_str(arg)
38
+ begin
39
+ arg.to_str
40
+ rescue
41
+ raise TypeError, "can't convert #{arg.class} into String"
42
+ end
43
+ end
44
+
45
+ #
46
+ # match eval's behavior
47
+ #
48
+ def handle_location(bind, *location)
49
+ case location.size
50
+ when 0
51
+ NATIVE_EVAL.call("[__FILE__, __LINE__]", bind)
52
+ when 1
53
+ [location.first, 1]
54
+ else
55
+ location
56
+ end
57
+ end
58
+
59
+ def fix_backtrace(backtrace)
60
+ backtrace.map! { |line|
61
+ LiveAST::Linker.strip_token line
62
+ }
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,107 @@
1
+ module LiveAST
2
+ module Attacher
3
+ VAR_NAME = :@_live_ast
4
+
5
+ def attach_to_proc(obj, ast)
6
+ obj.instance_variable_set(VAR_NAME, ast)
7
+ end
8
+
9
+ def fetch_proc_attachment(obj)
10
+ if obj.instance_variable_defined?(VAR_NAME)
11
+ obj.instance_variable_get(VAR_NAME)
12
+ end
13
+ end
14
+
15
+ def attach_to_method(klass, method, ast)
16
+ unless klass.instance_variable_defined?(VAR_NAME)
17
+ klass.instance_variable_set(VAR_NAME, {})
18
+ end
19
+ klass.instance_variable_get(VAR_NAME)[method] = ast
20
+ end
21
+
22
+ def fetch_method_attachment(klass, method)
23
+ if klass.instance_variable_defined?(VAR_NAME)
24
+ klass.instance_variable_get(VAR_NAME)[method]
25
+ end
26
+ end
27
+ end
28
+
29
+ module Linker
30
+ REVISION_TOKEN = "|ast@"
31
+
32
+ @caches = {}
33
+ @counter = "a"
34
+ @mutex = Mutex.new
35
+
36
+ class << self
37
+ include Attacher
38
+
39
+ def find_proc_ast(obj)
40
+ @mutex.synchronize do
41
+ fetch_proc_attachment(obj) or (
42
+ ast = find_ast(*obj.source_location) or raise FlushedError
43
+ attach_to_proc(obj, ast)
44
+ )
45
+ end
46
+ end
47
+
48
+ def find_method_ast(klass, name, *location)
49
+ @mutex.synchronize do
50
+ case ast = find_ast(*location)
51
+ when nil
52
+ fetch_method_attachment(klass, name) or raise FlushedError
53
+ else
54
+ attach_to_method(klass, name, ast)
55
+ end
56
+ end
57
+ end
58
+
59
+ def find_ast(*location)
60
+ raise NoSourceError unless location.size == 2
61
+ raise RawEvalError if location.first == "(eval)"
62
+ ast = fetch_from_cache(*location)
63
+ raise MultipleDefinitionsOnSameLineError if ast == :multiple
64
+ ast
65
+ end
66
+
67
+ def fetch_from_cache(file, line)
68
+ cache = @caches[file]
69
+ if !cache and !file.index(REVISION_TOKEN)
70
+ #
71
+ # File was loaded by 'require'.
72
+ # Play catch-up: assume it has not changed in the meantime.
73
+ #
74
+ _, cache = new_cache(Loader.read(file), file, 1, true)
75
+ end
76
+ cache.fetch_ast(line) if cache
77
+ end
78
+
79
+ #
80
+ # create a cache along with a uniquely-identifing key for it
81
+ #
82
+ def new_cache(contents, file, user_line, file_is_key)
83
+ key = file_is_key ? file : file + REVISION_TOKEN + @counter
84
+ cache = Cache.new(contents, user_line)
85
+ @caches[key] = cache
86
+ @counter.next!
87
+ return key, cache
88
+ end
89
+
90
+ def new_cache_synced(*args)
91
+ @mutex.synchronize do
92
+ new_cache(*args)
93
+ end
94
+ end
95
+
96
+ def flush_cache
97
+ @mutex.synchronize do
98
+ @caches.delete_if { |key, _| key.index REVISION_TOKEN }
99
+ end
100
+ end
101
+
102
+ def strip_token(file)
103
+ file.sub(/#{Regexp.quote REVISION_TOKEN}[a-z]+/, "")
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,69 @@
1
+ # encoding: us-ascii
2
+
3
+ module LiveAST
4
+ module Loader
5
+ MAGIC_COMMENT = /\A(?:#!.*?\n)?\s*\#.*(?:en)?coding\s*[:=]\s*([^\s;]+)/
6
+
7
+ class << self
8
+ def load(file, wrap)
9
+ file = find_file(file)
10
+
11
+ # guards to protect toplevel locals
12
+ header, footer, warnings_ok = header_footer(wrap)
13
+
14
+ parser_src = read(file)
15
+ evaler_src = header << parser_src << footer
16
+
17
+ run = lambda do
18
+ Evaler.eval(parser_src, evaler_src, TOPLEVEL_BINDING, file, 1)
19
+ end
20
+ warnings_ok ? run.call : suppress_warnings(&run)
21
+ true
22
+ end
23
+
24
+ def read(file)
25
+ contents = File.read(file, :encoding => "BINARY")
26
+ encoding = contents[MAGIC_COMMENT, 1] || "US-ASCII"
27
+ contents.force_encoding(encoding)
28
+ end
29
+
30
+ def header_footer(wrap)
31
+ if wrap
32
+ return "class << Object.new;", ";end", true
33
+ else
34
+ locals = NATIVE_EVAL.call("local_variables", TOPLEVEL_BINDING)
35
+
36
+ params = locals.empty? ? "" : ("|;" + locals.join(",") + "|")
37
+
38
+ return "lambda do #{params}", ";end.call", locals.empty?
39
+ end
40
+ end
41
+
42
+ def suppress_warnings
43
+ previous = $VERBOSE
44
+ $VERBOSE = nil
45
+ begin
46
+ yield
47
+ ensure
48
+ $VERBOSE = previous
49
+ end
50
+ end
51
+
52
+ def find_file(file)
53
+ if file.index Linker::REVISION_TOKEN
54
+ raise "refusing to load file with revision token: `#{file}'"
55
+ end
56
+ search_paths(file) or raise LoadError, "no such file to load -- #{file}"
57
+ end
58
+
59
+ def search_paths(file)
60
+ return file if File.file? file
61
+ $LOAD_PATH.each do |path|
62
+ target = path + "/" + file
63
+ return target if File.file? target
64
+ end
65
+ nil
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,48 @@
1
+ require 'ruby_parser'
2
+ require 'sexp_processor'
3
+
4
+ module LiveAST
5
+ class Parser < SexpProcessor
6
+ def parse(source)
7
+ @defs = {}
8
+ process RubyParser.new.parse(source)
9
+ @defs
10
+ end
11
+
12
+ def process_defn(sexp)
13
+ result = Sexp.new
14
+ result << sexp.shift
15
+ result << sexp.shift
16
+ result << process(sexp.shift)
17
+ result << process(sexp.shift)
18
+
19
+ store_sexp(result, sexp.line)
20
+ s()
21
+ end
22
+
23
+ def process_iter(sexp)
24
+ line = sexp[1].line
25
+
26
+ result = Sexp.new
27
+ result << sexp.shift
28
+ result << process(sexp.shift)
29
+ result << process(sexp.shift)
30
+ result << process(sexp.shift)
31
+
32
+ #
33
+ # ruby_parser bug: a method without args attached to a
34
+ # multi-line block reports the wrong line. workaround.
35
+ #
36
+ if result[1][3].size == 1
37
+ line = sexp.line
38
+ end
39
+
40
+ store_sexp(result, line)
41
+ s()
42
+ end
43
+
44
+ def store_sexp(sexp, line)
45
+ @defs[line] = @defs.has_key?(line) ? :multiple : sexp
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,14 @@
1
+ require 'live_ast/base'
2
+
3
+ module Kernel
4
+ alias_method :live_ast_original_load, :load
5
+
6
+ def load(file, wrap = false)
7
+ LiveAST.load(file, wrap)
8
+ end
9
+
10
+ class << self
11
+ remove_method :load
12
+ end
13
+ module_function :load
14
+ end
@@ -0,0 +1,21 @@
1
+ require 'live_ast/base'
2
+
3
+ module Kernel
4
+ private
5
+
6
+ alias_method :live_ast_original_raise, :raise
7
+
8
+ def raise(*args)
9
+ ex = begin
10
+ live_ast_original_raise(*args)
11
+ rescue Exception => ex
12
+ ex
13
+ end
14
+
15
+ ex.backtrace.reject! { |line| line.index __FILE__ }
16
+
17
+ LiveAST::Evaler.fix_backtrace ex.backtrace
18
+
19
+ live_ast_original_raise ex
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ require 'live_ast/base'
2
+
3
+ [Method, UnboundMethod].each do |klass|
4
+ klass.class_eval do
5
+ # Extract the AST of this object.
6
+ def to_ast
7
+ LiveAST::Linker.find_method_ast(owner, name, *source_location)
8
+ end
9
+ end
10
+ end
11
+
12
+ class Proc
13
+ # Extract the AST of this object.
14
+ def to_ast
15
+ LiveAST::Linker.find_proc_ast(self)
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ require 'ruby2ruby'
2
+
3
+ require 'live_ast/base'
4
+
5
+ [Method, UnboundMethod, Proc].each do |klass|
6
+ klass.class_eval do
7
+ # Generate ruby code which reflects the AST of this object.
8
+ def to_ruby
9
+ Ruby2Ruby.new.process(LiveAST.ast(self))
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module LiveAST
2
+ VERSION = "0.2.0"
3
+ end
data/lib/live_ast.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'live_ast/base'
2
+ require 'live_ast/to_ast'
3
+ require 'live_ast/ast_eval'
4
+ require 'live_ast/replace_load'
@@ -0,0 +1,11 @@
1
+ require_relative 'shared/main'
2
+
3
+ class AAB_ASTEvalFeatureTest < BaseTest
4
+ def test_require
5
+ assert !private_methods.include?(:ast_eval)
6
+
7
+ require 'live_ast/ast_eval'
8
+
9
+ assert private_methods.include?(:ast_eval)
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ require_relative 'shared/main'
2
+
3
+ class AAB_ASTLoadFeatureTest < BaseTest
4
+ def test_require
5
+ assert !private_methods.include?(:ast_load)
6
+
7
+ require 'live_ast/ast_load'
8
+
9
+ assert private_methods.include?(:ast_load)
10
+ end
11
+ end
@@ -0,0 +1,159 @@
1
+ require_relative 'shared/main'
2
+
3
+ # test for raise redefinition side-effects: unsort this TestCase from
4
+ # other TestCases.
5
+
6
+ define_unsorted_test_case "BacktraceTest", RegularTest do
7
+ def test_raise_in_eval
8
+ 3.times do
9
+ orig = exception_backtrace do
10
+ eval %{
11
+
12
+ raise
13
+
14
+
15
+ }, binding, "somewhere", 1000
16
+ end
17
+
18
+ live = exception_backtrace do
19
+ ast_eval %{
20
+
21
+ raise
22
+
23
+
24
+ }, binding, "somewhere", 1000
25
+ end
26
+
27
+ assert_equal orig.first, live.first
28
+ assert_match(/somewhere:1002/, live.first)
29
+ end
30
+ end
31
+
32
+ def test_raise_no_overrides
33
+ 3.times do
34
+ orig = exception_backtrace do
35
+ eval %{
36
+
37
+
38
+ raise
39
+
40
+ }, binding, __FILE__, (__LINE__ + 9)
41
+ end
42
+
43
+ live = exception_backtrace do
44
+ ast_eval %{
45
+
46
+
47
+ raise
48
+
49
+ }, binding
50
+ end
51
+
52
+ assert_equal orig.first, live.first
53
+ here = Regexp.quote __FILE__
54
+ assert_match(/#{here}/, live.first)
55
+ end
56
+ end
57
+
58
+ def test_raise_using_overrides
59
+ 3.times do
60
+ orig = exception_backtrace do
61
+ eval %{
62
+
63
+
64
+ raise
65
+
66
+ }, binding, __FILE__, (__LINE__ + 9)
67
+ end
68
+
69
+ live = exception_backtrace do
70
+ ast_eval %{
71
+
72
+
73
+ raise
74
+
75
+ }, binding, __FILE__, __LINE__
76
+ end
77
+
78
+ assert_equal orig.first, live.first
79
+ here = Regexp.quote __FILE__
80
+ assert_match(/#{here}/, live.first)
81
+ end
82
+ end
83
+
84
+ def test_raise_using_only_file_override
85
+ 3.times do
86
+ orig = exception_backtrace do
87
+ eval %{
88
+
89
+
90
+ raise
91
+
92
+ }, binding, __FILE__
93
+ end
94
+
95
+ live = exception_backtrace do
96
+ ast_eval %{
97
+
98
+
99
+ raise
100
+
101
+ }, binding, __FILE__
102
+ end
103
+
104
+ assert_equal orig.first, live.first
105
+ here = Regexp.quote __FILE__
106
+ assert_match(/#{here}/, live.first)
107
+ end
108
+ end
109
+
110
+ def test_raise_after_eval
111
+ raise_after_eval("raise", false)
112
+ raise_after_eval("1/0", false)
113
+
114
+ require 'live_ast/replace_raise'
115
+
116
+ raise_after_eval("raise", true)
117
+ raise_after_eval("1/0", false)
118
+ end
119
+
120
+ def raise_after_eval(code, will_succeed)
121
+ 3.times do
122
+ orig = eval %{
123
+
124
+ lambda { #{code} }
125
+
126
+
127
+ }, binding, "somewhere", 1000
128
+
129
+ live = ast_eval %{
130
+
131
+ lambda { #{code} }
132
+
133
+
134
+ }, binding, "somewhere", 1000
135
+
136
+ orig_top = exception_backtrace { orig.call }.first
137
+ live_top = exception_backtrace { live.call }.first
138
+
139
+ assert_equal orig_top, LiveAST::Linker.strip_token(live_top)
140
+
141
+ if will_succeed
142
+ assert_equal orig_top, live_top
143
+ here = Regexp.quote __FILE__
144
+ assert_match(/somewhere:1002/, live_top)
145
+ else
146
+ assert_not_equal orig_top, live_top
147
+ assert_match(/somewhere.*?:1002/, live_top)
148
+ end
149
+ end
150
+ end
151
+
152
+ def test_tokens_stripped
153
+ exception_backtrace do
154
+ ast_eval %{ ast_eval %{ ast_eval %{raise}, binding }, binding }, binding
155
+ end.each do |line|
156
+ assert_nil line.index(LiveAST::Linker::REVISION_TOKEN)
157
+ end
158
+ end
159
+ end