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