mvz-live_ast 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGES.rdoc +93 -0
  3. data/README.rdoc +419 -0
  4. data/Rakefile +21 -0
  5. data/devel/levitate.rb +853 -0
  6. data/devel/levitate_config.rb +4 -0
  7. data/lib/live_ast.rb +4 -0
  8. data/lib/live_ast/ast_eval.rb +11 -0
  9. data/lib/live_ast/ast_load.rb +15 -0
  10. data/lib/live_ast/base.rb +73 -0
  11. data/lib/live_ast/common.rb +48 -0
  12. data/lib/live_ast/error.rb +20 -0
  13. data/lib/live_ast/evaler.rb +32 -0
  14. data/lib/live_ast/full.rb +2 -0
  15. data/lib/live_ast/irb_spy.rb +43 -0
  16. data/lib/live_ast/linker.rb +122 -0
  17. data/lib/live_ast/loader.rb +60 -0
  18. data/lib/live_ast/reader.rb +26 -0
  19. data/lib/live_ast/replace_eval.rb +121 -0
  20. data/lib/live_ast/replace_load.rb +14 -0
  21. data/lib/live_ast/replace_raise.rb +18 -0
  22. data/lib/live_ast/ruby_parser.rb +36 -0
  23. data/lib/live_ast/ruby_parser/test.rb +197 -0
  24. data/lib/live_ast/ruby_parser/unparser.rb +13 -0
  25. data/lib/live_ast/to_ast.rb +26 -0
  26. data/lib/live_ast/to_ruby.rb +24 -0
  27. data/lib/live_ast/version.rb +3 -0
  28. data/test/ast_eval_feature_test.rb +11 -0
  29. data/test/ast_load_feature_test.rb +11 -0
  30. data/test/attr_test.rb +24 -0
  31. data/test/backtrace_test.rb +158 -0
  32. data/test/covert_define_method_test.rb +23 -0
  33. data/test/def_test.rb +35 -0
  34. data/test/define_method_test.rb +67 -0
  35. data/test/define_singleton_method_test.rb +15 -0
  36. data/test/encoding_test.rb +52 -0
  37. data/test/encoding_test/bad.rb +1 -0
  38. data/test/encoding_test/cp932.rb +6 -0
  39. data/test/encoding_test/default.rb +5 -0
  40. data/test/encoding_test/eucjp.rb +6 -0
  41. data/test/encoding_test/koi8.rb +6 -0
  42. data/test/encoding_test/koi8_shebang.rb +7 -0
  43. data/test/encoding_test/koi8_with_utf8bom.rb +6 -0
  44. data/test/encoding_test/usascii.rb +6 -0
  45. data/test/encoding_test/usascii_with_utf8bom.rb +6 -0
  46. data/test/encoding_test/utf8.rb +6 -0
  47. data/test/encoding_test/utf8bom.rb +6 -0
  48. data/test/encoding_test/utf8bom_only.rb +5 -0
  49. data/test/encoding_test/utf8dos.rb +6 -0
  50. data/test/encoding_test/utf8mac.rb +6 -0
  51. data/test/encoding_test/utf8mac_alt.rb +6 -0
  52. data/test/encoding_test/utf8unix.rb +6 -0
  53. data/test/error_test.rb +116 -0
  54. data/test/eval_test.rb +269 -0
  55. data/test/flush_cache_test.rb +98 -0
  56. data/test/irb_test.rb +25 -0
  57. data/test/lambda_test.rb +56 -0
  58. data/test/load_path_test.rb +78 -0
  59. data/test/load_test.rb +123 -0
  60. data/test/main.rb +140 -0
  61. data/test/nested_test.rb +29 -0
  62. data/test/noninvasive_test.rb +51 -0
  63. data/test/readme_test.rb +16 -0
  64. data/test/recursive_eval_test.rb +52 -0
  65. data/test/redefine_method_test.rb +83 -0
  66. data/test/reload_test.rb +105 -0
  67. data/test/replace_eval_test.rb +405 -0
  68. data/test/rubygems_test.rb +25 -0
  69. data/test/rubyspec_test.rb +39 -0
  70. data/test/singleton_test.rb +25 -0
  71. data/test/stdlib_test.rb +13 -0
  72. data/test/thread_test.rb +44 -0
  73. data/test/to_ast_feature_test.rb +15 -0
  74. data/test/to_ruby_feature_test.rb +15 -0
  75. data/test/to_ruby_test.rb +87 -0
  76. metadata +275 -0
@@ -0,0 +1,4 @@
1
+
2
+ if RUBY_ENGINE == "jruby"
3
+ Levitate.ruby_opts = %w[--1.9]
4
+ end
@@ -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 'live_ast/base'
2
+
3
+ module Kernel
4
+ private
5
+
6
+ # The same as +eval+ except that the binding argument is required
7
+ # and AST-accessible objects are created.
8
+ def ast_eval(*args)
9
+ LiveAST::Evaler.eval(args[0], *args)
10
+ end
11
+ 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,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,2 @@
1
+ require 'live_ast'
2
+ require 'live_ast/replace_eval'
@@ -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