live_ast 0.2.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.
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