Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

initial commit

  • Loading branch information...
commit d6a0cb241f85e199ffef5d8595e9a6ec8803106c 0 parents
@svenfuchs authored
Showing with 3,425 additions and 0 deletions.
  1. +20 −0 MIT-LICENSE
  2. +15 −0 NOTES
  3. +11 −0 README.markdown
  4. +19 −0 lib/ansi.rb
  5. +5 −0 lib/core_ext/object/meta_class.rb
  6. +6 −0 lib/core_ext/object/tap.rb
  7. +52 −0 lib/ripper/NOTES
  8. +54 −0 lib/ripper/erb/stripper.rb
  9. +73 −0 lib/ripper/ruby_builder.rb
  10. +46 −0 lib/ripper/ruby_builder/callbacks/args.rb
  11. +33 −0 lib/ripper/ruby_builder/callbacks/array.rb
  12. +53 −0 lib/ripper/ruby_builder/callbacks/assignment.rb
  13. +33 −0 lib/ripper/ruby_builder/callbacks/block.rb
  14. +29 −0 lib/ripper/ruby_builder/callbacks/call.rb
  15. +73 −0 lib/ripper/ruby_builder/callbacks/core.rb
  16. +31 −0 lib/ripper/ruby_builder/callbacks/hash.rb
  17. +21 −0 lib/ripper/ruby_builder/callbacks/operator.rb
  18. +24 −0 lib/ripper/ruby_builder/callbacks/params.rb
  19. +22 −0 lib/ripper/ruby_builder/callbacks/program.rb
  20. +77 −0 lib/ripper/ruby_builder/callbacks/scanner.rb
  21. +23 −0 lib/ripper/ruby_builder/callbacks/string.rb
  22. +31 −0 lib/ripper/ruby_builder/callbacks/symbol.rb
  23. +62 −0 lib/ripper/ruby_builder/stack.rb
  24. +33 −0 lib/ripper/ruby_builder/token.rb
  25. +19 −0 lib/ruby.rb
  26. +64 −0 lib/ruby/args.rb
  27. +31 −0 lib/ruby/array.rb
  28. +40 −0 lib/ruby/assignment.rb
  29. +17 −0 lib/ruby/assoc.rb
  30. +28 −0 lib/ruby/block.rb
  31. +25 −0 lib/ruby/call.rb
  32. +20 −0 lib/ruby/class.rb
  33. +80 −0 lib/ruby/composite.rb
  34. +6 −0 lib/ruby/const.rb
  35. +7 −0 lib/ruby/float.rb
  36. +48 −0 lib/ruby/hash.rb
  37. +6 −0 lib/ruby/identifier.rb
  38. +7 −0 lib/ruby/integer.rb
  39. +29 −0 lib/ruby/keyword.rb
  40. +19 −0 lib/ruby/method.rb
  41. +138 −0 lib/ruby/node.rb
  42. +45 −0 lib/ruby/operators.rb
  43. +35 −0 lib/ruby/params.rb
  44. +43 −0 lib/ruby/program.rb
  45. +35 −0 lib/ruby/string.rb
  46. +38 −0 lib/ruby/symbol.rb
  47. +16 −0 lib/ruby/token.rb
  48. +16 −0 lib/ruby/unsupported.rb
  49. +1 −0  test/all.rb
  50. +106 −0 test/fixtures/all.rb
  51. +15 −0 test/fixtures/source_1.rb
  52. +1 −0  test/fixtures/source_2.rb
  53. +22 −0 test/fixtures/template.html.erb
  54. +1 −0  test/ripper2ruby/all.rb
  55. +49 −0 test/ripper2ruby/builder_test.rb
  56. +48 −0 test/ripper2ruby/context_test.rb
  57. +29 −0 test/ripper2ruby/erb_stripper_test.rb
  58. +1 −0  test/ripper2ruby/nodes/all.rb
  59. +192 −0 test/ripper2ruby/nodes/args_test.rb
  60. +99 −0 test/ripper2ruby/nodes/array_test.rb
  61. +68 −0 test/ripper2ruby/nodes/assignment_test.rb
  62. +29 −0 test/ripper2ruby/nodes/block_test.rb
  63. +177 −0 test/ripper2ruby/nodes/call_test.rb
  64. +53 −0 test/ripper2ruby/nodes/const_test.rb
  65. +110 −0 test/ripper2ruby/nodes/hash_test.rb
  66. +102 −0 test/ripper2ruby/nodes/keyword_test.rb
  67. +39 −0 test/ripper2ruby/nodes/method_test.rb
  68. +74 −0 test/ripper2ruby/nodes/node_test.rb
  69. +15 −0 test/ripper2ruby/nodes/nodes_test.rb
  70. +43 −0 test/ripper2ruby/nodes/numbers_test.rb
  71. +189 −0 test/ripper2ruby/nodes/operators_test.rb
  72. +130 −0 test/ripper2ruby/nodes/string_test.rb
  73. +90 −0 test/ripper2ruby/nodes/symbol_test.rb
  74. +13 −0 test/ripper2ruby/nodes/tmp_test.rb
  75. +26 −0 test/ripper2ruby/nodes/token_test.rb
  76. +1 −0  test/ripper2ruby/test_helper.rb
  77. +44 −0 test/test_helper.rb
20 MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2009 Sven Fuchs <svenfuchs@artweb-design.de>
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15 NOTES
@@ -0,0 +1,15 @@
+
+# MISSING STUFF
+
+# range
+# .. ...
+#
+# special:
+# defined?
+#
+# hash:
+# [ ] [ ]=
+#
+# control flow:
+# if unless while until
+# begin/end
11 README.markdown
@@ -0,0 +1,11 @@
+Ripper2Ruby
+===========
+
+Similar to ruby2ruby this library allows to parse Ruby code, modify and
+recompile it back to Ruby.
+
+Differences:
+
+* uses Ripper for parsing (shipped with Ruby 1.9)
+* produces a full object-oriented representation of the Ruby code
+
19 lib/ansi.rb
@@ -0,0 +1,19 @@
+module Ansi
+ COLORS = { :red =>";31", :yellow => ";33", :green => ";32" }
+ STYLES = { :bold => ";1", :underline => ";4" }
+
+ def ansi_format(text, formats)
+ res = "\e[0"
+ if formats.is_a?(Array) || formats.is_a?(Hash)
+ COLORS.each { |k,v| res += v if formats.include?(k) }
+ STYLES.each { |k,v| res += v if formats.include?(k) }
+ elsif formats.is_a?(Symbol)
+ COLORS.each { |k,v| res += v if formats == k }
+ STYLES.each { |k,v| res += v if formats == k }
+ elsif formats.respond_to?(:to_sym)
+ COLORS.each { |k,v| res += v if formats.to_sym == k }
+ STYLES.each { |k,v| res += v if formats.to_sym == k }
+ end
+ res += "m" + text.to_s + "\e[0m"
+ end
+end
5 lib/core_ext/object/meta_class.rb
@@ -0,0 +1,5 @@
+class Object
+ def meta_class
+ (class << self; self; end)
+ end
+end
6 lib/core_ext/object/tap.rb
@@ -0,0 +1,6 @@
+class Object
+ def tap
+ yield self
+ self
+ end
+end
52 lib/ripper/NOTES
@@ -0,0 +1,52 @@
+TODO
+- invent a StatementsList so that it can have params
+
+
+Ripper::SCANNER_EVENTS
+
+:CHAR
+:__end__
+:backref
+:backtick
+:comma
+:comment
+:const
+:cvar
+:embdoc
+:embdoc_beg
+:embdoc_end
+:embexpr_beg
+:embexpr_end
+:embvar
+:float
+:gvar
+:heredoc_beg
+:heredoc_end
+:ident
+:ignored_nl "\n" # => [:@ignored_nl, "\n", [1, 0]]
+:int
+:ivar
+:kw
+:label
+:lbrace
+:lbracket "[a]" # => [:@lbracket, "[", [1, 0]]
+:lparen "foo(b)" # => [:@lparen, "(", [1, 3]]
+:nl "a\n" # => [:@nl, "\n", [1, 1]]
+:op
+:period
+:qwords_beg "%w(a b c)" # => [:@qwords_beg, "%w(", [1, 0]]
+:rbrace
+:rbracket "[a]" # => [:@rbracket, "[", [1, 2]]
+:regexp_beg
+:regexp_end
+:rparen "foo(b)" # => [:@rparen, ")", [1, 5]]
+:semicolon
+:sp " a" # => [:@sp, " ", [1, 0]]
+:symbeg ":a" # => [:@symbeg, ":'", [1, 1]]
+:tlambda
+:tlambeg
+:tstring_beg "'a'; %(b)" # => [:@tstring_beg, "'", [1, 0]], [:@tstring_beg, "%(", [1, 5]]
+:tstring_content '"a"' # => [:@tstring_content, "a", [1, 1]]
+:tstring_end "'a'; %(b)" # => [:@tstring_end, "'", [1, 2]], [:@tstring_end, ")", [1, 8]]
+:words_beg
+:words_sep
54 lib/ripper/erb/stripper.rb
@@ -0,0 +1,54 @@
+# replaces html and erb tags with whitespace so that we can parse the result
+# as pure ruby preserving the exact positions of tokens in the original erb
+# source code
+
+require 'erb'
+$KCODE = 'u'
+
+class Ripper
+ module Erb
+ class Scanner < ERB::Compiler::Scanner
+ def scan
+ stag_reg = /(.*?)(^[ \t]*<%%|<%=|<%#|<%-|<%|\z)/m
+ etag_reg = /(.*?)(%%>|\-%>|%>|\z)/m
+ scanner = StringScanner.new(@src)
+ while !scanner.eos?
+ scanner.scan(@stag ? etag_reg : stag_reg)
+ yield(scanner[1]) unless scanner[1].nil?
+ yield(scanner[2]) unless scanner[2].nil?
+ end
+ end
+ end
+ ERB::Compiler::Scanner.regist_scanner(Scanner, nil, false)
+
+ class Stripper
+ def to_ruby(source)
+ result = ''
+ comment = false
+ scanner = ERB::Compiler.new(nil).make_scanner(source)
+ scanner.scan do |token|
+ comment = true if token == '<%#'
+ if scanner.stag.nil?
+ result << to_whitespace(token)
+ scanner.stag = token if ['<%', '<%-', '<%=', '<%#'].include?(token)
+ elsif ['%>', '-%>'].include?(token)
+ result << to_whitespace(token.gsub(/>/, ';'))
+ scanner.stag = nil
+ else
+ result << (comment ? to_whitespace(token) : token)
+ comment = false
+ end
+ end
+ result
+ end
+
+ def to_whitespace(str)
+ str.gsub(/[^\s;]/, ' ')
+ end
+
+ # def content_dump(string)
+ # string.split("\n").map { |s| to_whitespace(s) }.join("\n")
+ # end
+ end
+ end
+end
73 lib/ripper/ruby_builder.rb
@@ -0,0 +1,73 @@
+require 'ripper'
+require 'ruby'
+require 'ripper/ruby_builder/token'
+require 'ripper/ruby_builder/stack'
+
+Dir[File.dirname(__FILE__) + '/ruby_builder/callbacks/*.rb'].each { |file| require file }
+
+class Ripper
+ class RubyBuilder < Ripper::SexpBuilder
+ class << self
+ def build(src)
+ new(src).parse
+ end
+ end
+
+ WHITESPACE = [:@sp, :@nl, :@ignored_nl]
+
+ include Core, Program, Block, Params, Call, String, Symbol, Hash, Array,
+ Args, Assignment, Operator, Scanner
+
+ attr_reader :src, :filename, :stack
+
+ def initialize(src, filename = nil, lineno = nil)
+ @src = src || filename && File.read(filename)
+ @filename = filename
+ @whitespace = ''
+ @stack = []
+ @_stack_ignore_stack = [[]]
+ @stack = Stack.new
+ super
+ end
+
+ def position
+ [lineno - 1, column]
+ end
+
+ def push(sexp)
+ stack.push(Token.new(*sexp))
+ end
+
+ protected
+
+ def build_identifier(token)
+ end
+
+ def build_token(token)
+ Ruby::Token.new(token.value, token.position, token.whitespace) if token
+ end
+
+ def pop(*types)
+ stack.pop(*types)
+ end
+
+ def pop_delim(type, options = {})
+ pop_delims(type, options).first
+ end
+
+ def pop_delims(*types)
+ options = types.last.is_a?(::Hash) ? types.pop : {}
+ stack_ignore(*WHITESPACE) do
+ types.map { |type| pop(type, options).map { |token| build_token(token) } }.flatten.compact
+ end
+ end
+
+ def pop_whitespace
+ pop(*WHITESPACE).reverse.map { |token| token.value }.join
+ end
+
+ def stack_ignore(*types, &block)
+ stack.ignore_types(*types, &block)
+ end
+ end
+end
46 lib/ripper/ruby_builder/callbacks/args.rb
@@ -0,0 +1,46 @@
+class Ripper
+ class RubyBuilder < Ripper::SexpBuilder
+ module Args
+ def on_arg_paren(args)
+ args ||= Ruby::ArgsList.new # will be nil when call has an empty arglist, e.g. I18n.t()
+
+ pop_delim(:@rparen).tap { |r| args.rdelim = r if r }
+ pop_delim(:@lparen).tap { |l| args.ldelim = l if l }
+
+ args
+ end
+
+ def on_args_add_block(args, block)
+ rdelim = pop_delim(:@rparen, :@rbracket) # also used by :aref_field assignments, i.e. array[0] = :foo
+ operator = pop_delim(:@op, :value => '&')
+ separators = pop_delims(:@comma)
+ ldelim = pop_delim(:@lparen, :@lbracket)
+
+ args << Ruby::BlockArg.new(block, operator) if block
+ args.separators += separators if separators
+ args.ldelim = ldelim if ldelim
+ args.rdelim = rdelim if rdelim
+ args
+ end
+
+ def on_args_add_star(args, arg)
+ stack_ignore(:@rparen, :@period) do
+ pop_delims(:@comma).tap { |s| args.separators += s.reverse }
+ star = pop_delim(:@op)
+ end
+ args << arg
+ args
+ end
+
+ def on_args_add(args, arg)
+ pop_delims(:@comma).tap { |s| args.separators += s.reverse }
+ args << arg
+ args
+ end
+
+ def on_args_new
+ Ruby::ArgsList.new
+ end
+ end
+ end
+end
33 lib/ripper/ruby_builder/callbacks/array.rb
@@ -0,0 +1,33 @@
+class Ripper
+ class RubyBuilder < Ripper::SexpBuilder
+ module Array
+ # elements and separators are collected in on_args_add
+ # confusingly ripper throws the same events
+
+ def on_array(elements)
+ rdelim, ldelim = pop_delims(:@rbracket, :@lbracket)
+ separators = elements ? elements.separators : []
+ elements = elements ? elements.to_a : []
+ Ruby::Array.new(elements, ldelim, rdelim, separators)
+ end
+
+ def on_qwords_new(*args)
+ Ruby::Array.new(nil, pop_delim(:@qwords_beg))
+ end
+
+ def on_qwords_add(array, arg)
+ tokens = pop_delims(:@words_sep)
+
+ array.separators += tokens.select { |t| t.token =~ /^\s*$/ }
+ array.rdelim = (tokens - array.separators).first
+ array << arg
+ array
+ end
+
+ def on_aref_field(target, args)
+ Ruby::Call.new(target, nil, nil, args)
+ end
+ end
+ end
+end
+
53 lib/ripper/ruby_builder/callbacks/assignment.rb
@@ -0,0 +1,53 @@
+class Ripper
+ class RubyBuilder < Ripper::SexpBuilder
+ module Assignment
+ # simple assignments, e.g. a = b
+ def on_assign(left, right)
+ Ruby::Assignment.new(left, right, pop_delim(:@op, :value => '='))
+ end
+
+ # mass assignments, e.g. a, b = c, d
+ def on_massign(left, right)
+ Ruby::Assignment.new(left, right, pop_delim(:@op, :value => '='))
+ end
+
+ def on_mlhs_new
+ Ruby::MultiAssignment.new(:left, pop_delim(:@lparen))
+ end
+
+ def on_mlhs_add(assignment, ref)
+ separator = pop_delim(:@comma)
+ assignment.separators << separator if separator
+
+ assignment << ref
+ assignment
+ end
+
+ def on_mlhs_paren(arg)
+ arg.rdelim = pop_delim(:@rparen) if arg.is_a?(Ruby::MultiAssignment)
+ arg
+ end
+
+ def on_mrhs_new
+ separators = pop_delims(:@comma).reverse
+ star = pop_delim(:@op, :value => '*')
+ Ruby::MultiAssignment.new(:right, nil, nil, separators, star)
+ end
+
+ def on_mrhs_new_from_args(args)
+ # separators = pop_delims(:@comma).reverse
+ Ruby::MultiAssignment.new(:right, nil, nil, args.separators, nil, args.args)
+ end
+
+ def on_mrhs_add(assignment, ref)
+ assignment << ref
+ assignment
+ end
+
+ def on_mrhs_add_star(assignment, ref)
+ assignment << ref
+ assignment
+ end
+ end
+ end
+end
33 lib/ripper/ruby_builder/callbacks/block.rb
@@ -0,0 +1,33 @@
+class Ripper
+ class RubyBuilder < Ripper::SexpBuilder
+ module Block
+ def on_body_stmt(statements, *something)
+ Ruby::Body.new(statements.compact)
+ end
+
+ def on_method_add_arg(call, args)
+ call.arguments = args
+ call
+ end
+
+ def on_method_add_block(call, block)
+ block.rdelim = pop_delim(:@kw, :value => 'end') || pop_delim(:@rbrace)
+ block.ldelim = pop_delim(:@kw, :value => 'do') || pop_delim(:@lbrace)
+ call.block = block
+ call
+ end
+
+ def on_do_block(params, statements)
+ Ruby::Block.new(statements.compact, params)
+ end
+
+ def on_brace_block(params, statements)
+ Ruby::Block.new(statements.compact, params)
+ end
+
+ def on_block_var(params, something)
+ params
+ end
+ end
+ end
+end
29 lib/ripper/ruby_builder/callbacks/call.rb
@@ -0,0 +1,29 @@
+class Ripper
+ class RubyBuilder < Ripper::SexpBuilder
+ module Call
+ def on_command(identifier, args)
+ Ruby::Call.new(nil, nil, identifier, args)
+ end
+
+ def on_command_call(target, separator, identifier, args)
+ separator = pop_delim(:@period)
+ Ruby::Call.new(target, separator, identifier, args)
+ end
+
+ def on_call(target, separator, identifier)
+ separator = pop_delim(:@period)
+ Ruby::Call.new(target, separator, identifier)
+ end
+
+ def on_fcall(identifier)
+ Ruby::Call.new(nil, nil, identifier)
+ end
+
+ # assignment methods, e.g. a.b = :c
+ def on_field(target, separator, identifier)
+ separator = stack_ignore(:@op) { pop_delim(:@period) }
+ Ruby::Call.new(target, separator, identifier)
+ end
+ end
+ end
+end
73 lib/ripper/ruby_builder/callbacks/core.rb
@@ -0,0 +1,73 @@
+class Ripper
+ class RubyBuilder < Ripper::SexpBuilder
+ module Core
+ def on_ident(token)
+ Ruby::Identifier.new(token, position, pop_whitespace)
+ end
+
+ def on_kw(token)
+ if %w(class def do end not and or).include?(token)
+ return push(super)
+ else
+ Ruby::Keyword.new(token, position, pop_whitespace)
+ end
+ end
+
+ def on_cvar(token)
+ Ruby::Identifier.new(token, position)
+ end
+
+ def on_ivar(token)
+ Ruby::Identifier.new(token, position)
+ end
+
+ def on_gvar(token)
+ Ruby::Identifier.new(token, position)
+ end
+
+ def on_int(token)
+ Ruby::Integer.new(token, position, pop_whitespace)
+ end
+
+ def on_float(token)
+ Ruby::Float.new(token, position, pop_whitespace)
+ end
+
+ def on_const(token)
+ Ruby::Const.new(token, position, pop_whitespace)
+ end
+
+ def on_const_path_ref(parent, const)
+ separator = stack_ignore(:@period) { pop_delim(:@op, :value => '::') }
+ Ruby::Call.new(parent, separator, const) # TODO maybe do Ruby::ConstRef < Ruby::Call instead
+ end
+
+ def on_class(const, super_class, body)
+ rdelim = pop_delim(:@kw, :value => 'end')
+ operator = pop_delim(:@op)
+ ldelim = pop_delim(:@kw, :value => 'class')
+ Ruby::Class.new(const, operator, super_class, body, ldelim, rdelim)
+ end
+
+ def on_def(identifier, params, body)
+ identifier = identifier.to_identifier if identifier.respond_to?(:to_identifier)
+ rdelim, ldelim = stack_ignore(:@op, :@comma, :@lparen, :@rparen) do
+ pop_delims(:@kw, :value => %w(def end))
+ end
+ Ruby::Method.new(identifier, params, body, ldelim, rdelim)
+ end
+
+ def on_const_ref(const)
+ const # not sure what to do here
+ end
+
+ def on_var_ref(ref)
+ ref # not sure what to do here
+ end
+
+ def on_var_field(field)
+ field # not sure what to do here
+ end
+ end
+ end
+end
31 lib/ripper/ruby_builder/callbacks/hash.rb
@@ -0,0 +1,31 @@
+class Ripper
+ class RubyBuilder < Ripper::SexpBuilder
+ module Hash
+ def on_hash(assocs)
+ separators = pop_delims(:@rbrace, :@comma, :@lbrace).reverse
+ ldelim, rdelim = separators.shift, separators.pop
+
+ Ruby::Hash.new(assocs, ldelim, rdelim, separators)
+ end
+
+ def on_assoclist_from_args(args)
+ args
+ end
+
+ def on_bare_assoc_hash(assocs)
+ separators = stack_ignore(:@rparen) do
+ pop_delims(:@comma, :max => assocs.length - 1).reverse
+ end
+
+ Ruby::Hash.new(assocs, nil, nil, separators)
+ end
+
+ def on_assoc_new(key, value)
+ stack_ignore(:@rbrace, :@rparen, :@comma) do
+ assoc = Ruby::Assoc.new(key, value, pop_delim(:@op))
+ end
+ end
+ end
+ end
+end
+
21 lib/ripper/ruby_builder/callbacks/operator.rb
@@ -0,0 +1,21 @@
+class Ripper
+ class RubyBuilder < Ripper::SexpBuilder
+ module Operator
+ def on_unary(operator, operand)
+ operator = pop_delim(:@op) || pop_delim(:@kw, :value => 'not')
+ Ruby::Unary.new(operator, operand)
+ end
+
+ def on_binary(left, operator, right)
+ stack_ignore(:@rparen) do
+ operator = pop_delim(:@op) || pop_delim(:@kw, :value => %w(and or))
+ end
+ Ruby::Binary.new(operator, left, right)
+ end
+
+ def on_ifop(condition, left, right)
+ Ruby::IfOp.new(condition, left, right, pop_delims(:@op).reverse)
+ end
+ end
+ end
+end
24 lib/ripper/ruby_builder/callbacks/params.rb
@@ -0,0 +1,24 @@
+class Ripper
+ class RubyBuilder < Ripper::SexpBuilder
+ module Params
+ def on_params(params, optional_params, rest_param, *something) # block_param?
+ params = (Array(params) + Array(optional_params) << rest_param).flatten.compact
+
+ rdelim = pop_delim(:@rparen) || pop_delim(:@op, :value => '|')
+ separators = pop_delims(:@comma)
+ ldelim = pop_delim(:@lparen) ||pop_delim(:@op, :value => '|')
+
+ Ruby::ParamsList.new(params, '', ldelim, rdelim, separators)
+ end
+
+ def on_rest_param(identifier)
+ star = pop_delim(:@op, :value => '*')
+ Ruby::RestParam.new(identifier.token, identifier.position, star)
+ end
+
+ def on_paren(params)
+ params
+ end
+ end
+ end
+end
22 lib/ripper/ruby_builder/callbacks/program.rb
@@ -0,0 +1,22 @@
+class Ripper
+ class RubyBuilder < Ripper::SexpBuilder
+ module Program
+ def on_program(statements)
+ Ruby::Program.new(src, filename, statements)
+ end
+
+ def on_stmts_add(target, statement)
+ target << statement
+ target
+ end
+
+ def on_stmts_new
+ []
+ end
+
+ def on_void_stmt
+ nil # what's this?
+ end
+ end
+ end
+end
77 lib/ripper/ruby_builder/callbacks/scanner.rb
@@ -0,0 +1,77 @@
+class Ripper
+ class RubyBuilder < Ripper::SexpBuilder
+ module Scanner
+ def on_sp(*args)
+ push(super)
+ end
+
+ def on_nl(*args)
+ push(super)
+ end
+
+ def on_ignored_nl(*args)
+ push(super)
+ end
+
+ def on_symbeg(*args)
+ push(super)
+ end
+
+ def on_tstring_beg(*args)
+ push(super)
+ end
+
+ def on_tstring_end(*args)
+ push(super)
+ end
+
+ def on_lparen(*args)
+ push(super)
+ end
+
+ def on_rparen(*args)
+ push(super)
+ end
+
+ def on_lbracket(*args)
+ push(super)
+ end
+
+ def on_rbracket(*args)
+ push(super)
+ end
+
+ def on_lbrace(*args)
+ push(super)
+ end
+
+ def on_rbrace(*args)
+ push(super)
+ end
+
+ def on_qwords_beg(*args)
+ push(super)
+ end
+
+ def on_op(*args)
+ push(super)
+ end
+
+ def on_comma(*args)
+ push(super)
+ end
+
+ def on_words_sep(*args)
+ push(super)
+ end
+
+ def on_period(*args)
+ push(super)
+ end
+
+ # def on_words_beg(*args)
+ # super.tap { |result| p result }
+ # end
+ end
+ end
+end
23 lib/ripper/ruby_builder/callbacks/string.rb
@@ -0,0 +1,23 @@
+class Ripper
+ class RubyBuilder < Ripper::SexpBuilder
+ module String
+ def on_string_literal(string)
+ string.rdelim = pop_delim(:@tstring_end)
+ string
+ end
+
+ def on_string_add(string, content)
+ string << content and string
+ end
+
+ def on_string_content
+ ldelim = pop_delim(:@tstring_beg)
+ Ruby::String.new(ldelim)
+ end
+
+ def on_tstring_content(token)
+ Ruby::StringContent.new(token, position)
+ end
+ end
+ end
+end
31 lib/ripper/ruby_builder/callbacks/symbol.rb
@@ -0,0 +1,31 @@
+class Ripper
+ class RubyBuilder < Ripper::SexpBuilder
+ module Symbol
+ def on_symbol_literal(symbol)
+ symbol
+ end
+
+ def on_symbol(token)
+ token = token.token if token.respond_to?(:token)
+ token = token.value if token.respond_to?(:value)
+
+ ldelim = pop_delim(:@symbeg)
+ Ruby::Symbol.new(token, ldelim)
+ end
+
+ def on_dyna_symbol(symbol)
+ symbol.rdelim = pop_delim(:@tstring_end)
+ symbol
+ end
+
+ def on_xstring_add(string, content)
+ string.tap { |s| s << content }
+ end
+
+ def on_xstring_new
+ ldelim = pop_delim(:@symbeg)
+ Ruby::DynaSymbol.new(ldelim)
+ end
+ end
+ end
+end
62 lib/ripper/ruby_builder/stack.rb
@@ -0,0 +1,62 @@
+class Ripper
+ class RubyBuilder < Ripper::SexpBuilder
+ class Stack < ::Array
+ def initialize
+ @ignore_stack = []
+ end
+
+ def push(token)
+ while !token.whitespace? && last && last.whitespace?
+ token.whitespace = _pop.value + token.whitespace
+ end
+ self << token
+ token
+ end
+
+ alias :_pop :pop
+ def pop(*types)
+ options = types.last.is_a?(::Hash) ? types.pop : {}
+ max = options[:max]
+ value = options[:value]
+ tokens, ignored = [], []
+
+ while !empty? && !(max && tokens.length >= max)
+ if types.include?(last.type) && value_matches?(last, value)
+ tokens << super()
+ elsif ignore?(last.type)
+ ignored << super()
+ else
+ break
+ end
+ end
+
+ ignored.reverse.each { |token| push(token) }
+ tokens
+ end
+
+ def ignore?(type)
+ @ignore_stack.flatten.include?(type)
+ end
+
+ def ignore_types(*types)
+ @ignore_stack.push(types)
+ result = yield
+ @ignore_stack.pop
+ result
+ end
+
+ protected
+
+ def value_matches?(token, value)
+ case value
+ when nil
+ true
+ when ::Array
+ value.include?(token.value)
+ else
+ token.value == value
+ end
+ end
+ end
+ end
+end
33 lib/ripper/ruby_builder/token.rb
@@ -0,0 +1,33 @@
+class Ripper
+ class RubyBuilder < Ripper::SexpBuilder
+ class Token
+ attr_accessor :type, :value, :whitespace, :row, :column
+
+ def initialize(type, value = nil, position = nil)
+ @type = type
+ @value = value
+ @whitespace = ''
+ if position
+ @row = position[0] - 1
+ @column = position[1]
+ end
+ end
+
+ def position
+ [row, column]
+ end
+
+ def whitespace?
+ WHITESPACE.include?(type)
+ end
+
+ def to_sexp
+ [type, value, [row + 1, column]]
+ end
+
+ def to_identifier
+ Ruby::Identifier.new(value, position, whitespace)
+ end
+ end
+ end
+end
19 lib/ruby.rb
@@ -0,0 +1,19 @@
+require 'ansi'
+
+Dir[File.dirname(__FILE__) + '/ruby/*.rb'].each do |file|
+ require "ruby/#{File.basename(file)}"
+end
+
+module Ruby
+ @@context_width = 2
+
+ class << self
+ def context_width
+ @@context_width
+ end
+
+ def context_width=(num)
+ @@context_width = num
+ end
+ end
+end
64 lib/ruby/args.rb
@@ -0,0 +1,64 @@
+require 'ruby/node'
+
+module Ruby
+ class ArgsList < Node
+ child_accessor :args, :separators, :ldelim, :rdelim
+
+ def initialize
+ self.args = []
+ self.separators = []
+ end
+
+ def <<(arg)
+ # unless arg.is_a?(Node)
+ # arg = from_native(arg, nil, ' ')
+ # separators << from_native(' ,')
+ # end
+ super
+ end
+
+ def []=(ix, arg)
+ arg = from_native(arg, nil, self[ix].whitespace) unless arg.is_a?(Node)
+ arg.position = self[ix].position
+ super
+ end
+
+ def pop
+ [args.pop, separators.pop]
+ end
+
+ def options
+ last.is_a?(Ruby::Hash) ? last : nil # TODO fix position!
+ end
+
+ def set_option(key, value)
+ if options.nil?
+ self << { key => value }
+ else
+ options[key] = value
+ end
+ end
+
+ def nodes
+ [ldelim, zip(separators), rdelim].flatten.compact
+ end
+
+ def method_missing(method, *args, &block)
+ self.args.respond_to?(method) ? self.args.send(method, *args, &block) : super
+ end
+ end
+
+ class BlockArg < Node
+ child_accessor :arg
+ attr_accessor :ldelim
+
+ def initialize(arg, ldelim)
+ self.arg = arg
+ self.ldelim = ldelim
+ end
+
+ def nodes
+ [ldelim, arg]
+ end
+ end
+end
31 lib/ruby/array.rb
@@ -0,0 +1,31 @@
+require 'ruby/node'
+
+module Ruby
+ class Array < Node
+ child_accessor :elements, :separators, :ldelim, :rdelim
+
+ def initialize(elements, ldelim, rdelim = nil, separators = nil)
+ self.ldelim = ldelim
+ self.rdelim = rdelim
+ self.elements = elements || []
+ self.separators = separators || []
+ end
+
+ def <<(element)
+ elements << element
+ self
+ end
+
+ def value
+ map { |element| element.value }
+ end
+
+ def nodes
+ [ldelim, zip(separators), rdelim].flatten.compact
+ end
+
+ def method_missing(method, *args, &block)
+ elements.respond_to?(method) ? elements.send(method, *args, &block) : super
+ end
+ end
+end
40 lib/ruby/assignment.rb
@@ -0,0 +1,40 @@
+require 'ruby/node'
+
+module Ruby
+ class Assignment < Node
+ child_accessor :left, :right, :operator
+
+ def initialize(left, right, operator)
+ self.left = left
+ self.right = right
+ self.operator = operator
+ super(left.position)
+ end
+
+ def nodes
+ [left, operator, right]
+ end
+ end
+
+ class MultiAssignment < Node
+ attr_accessor :kind
+ child_accessor :refs, :separators, :ldelim, :rdelim, :star
+
+ def initialize(kind, ldelim = nil, rdelim = nil, separators = [], star = nil, refs = [])
+ self.kind = kind
+ self.ldelim = ldelim
+ self.rdelim = rdelim
+ self.star = star
+ self.separators = separators
+ self.refs = refs
+ end
+
+ def nodes
+ [ldelim, star, zip(separators), rdelim].flatten.compact
+ end
+
+ def method_missing(method, *args, &block)
+ @refs.respond_to?(method) ? @refs.send(method, *args, &block) : super
+ end
+ end
+end
17 lib/ruby/assoc.rb
@@ -0,0 +1,17 @@
+require 'ruby/node'
+
+module Ruby
+ class Assoc < Node
+ child_accessor :key, :value, :operator
+
+ def initialize(key, value, operator)
+ self.key = key
+ self.value = value
+ self.operator = operator
+ end
+
+ def nodes
+ [key, operator, value]
+ end
+ end
+end
28 lib/ruby/block.rb
@@ -0,0 +1,28 @@
+require 'ruby/params'
+
+module Ruby
+ class Body < Node
+ child_accessor :statements
+
+ def initialize(statements)
+ self.statements = statements
+ end
+
+ def nodes
+ statements
+ end
+ end
+
+ class Block < Body
+ child_accessor :params, :rdelim, :ldelim
+
+ def initialize(statements, params)
+ self.params = params
+ super(statements)
+ end
+
+ def nodes
+ [ldelim, params, super, rdelim].flatten.compact
+ end
+ end
+end
25 lib/ruby/call.rb
@@ -0,0 +1,25 @@
+require 'ruby/args'
+
+module Ruby
+ class Call < Node
+ child_accessor :identifier, :separator, :target, :arguments, :block
+
+ def initialize(target, separator, identifier, arguments = nil, block = nil)
+ target = Unsupported.new(target) if target && !target.is_a?(Node)
+
+ self.target = target
+ self.separator = separator
+ self.identifier = identifier
+ self.arguments = arguments
+ self.block = block
+ end
+
+ def token
+ identifier.token
+ end
+
+ def nodes
+ [target, separator, identifier, arguments, block].flatten.compact
+ end
+ end
+end
20 lib/ruby/class.rb
@@ -0,0 +1,20 @@
+require 'ruby/const'
+
+module Ruby
+ class Class < Node
+ child_accessor :const, :operator, :super_class, :body, :ldelim, :rdelim
+
+ def initialize(const, operator, super_class, body, ldelim, rdelim)
+ self.const = const
+ self.operator = operator
+ self.super_class = super_class
+ self.body = body
+ self.ldelim = ldelim
+ self.rdelim = rdelim
+ end
+
+ def nodes
+ [ldelim, const, operator, super_class, body, rdelim]
+ end
+ end
+end
80 lib/ruby/composite.rb
@@ -0,0 +1,80 @@
+module Ruby
+ module Composite
+ class Array < ::Array
+ include Composite
+
+ def initialize(objects = [])
+ objects.each { |object| self << object }
+ end
+
+ def detect
+ each { |element| return element if yield(element) }
+ end
+
+ def <<(object)
+ object = Unsupported.new(object) if object && !object.is_a?(Node)
+ object.parent = self.parent unless object.parent == self.parent
+ super
+ end
+
+ def []=(ix, object)
+ parent.children.delete(self[ix])
+ object.parent = parent
+ super
+ end
+
+ def pop
+ object = super
+ parent.children.delete(object)
+ object
+ end
+
+ def parent=(parent)
+ each { |object| object.parent = parent }
+ @parent = parent
+ end
+
+ def +(other)
+ self.dup.tap { |dup| other.each { |object| dup << object } }
+ end
+ end
+
+ def self.included(target)
+ target.class_eval do
+ class << self
+ def child_accessor(*names, &block)
+ names.each do |name|
+ attr_reader name
+ define_method("#{name}=") do |value|
+ value = Composite::Array.new(value) if value.is_a?(::Array)
+ value.parent = self if value
+ instance_variable_set(:"@#{name}", value)
+ yield(value) if block_given?
+ end
+ end
+ end
+ end
+ end
+ end
+
+ attr_reader :parent
+
+ def parent=(parent)
+ @parent.children.delete(self) if @parent
+ @parent = parent
+ parent.children << self if parent && !parent.children.include?(self)
+ end
+
+ def children
+ @children ||= []
+ end
+
+ def root?
+ parent.nil?
+ end
+
+ def root
+ root? ? self : parent.root
+ end
+ end
+end
6 lib/ruby/const.rb
@@ -0,0 +1,6 @@
+require 'ruby/identifier'
+
+module Ruby
+ class Const < Identifier
+ end
+end
7 lib/ruby/float.rb
@@ -0,0 +1,7 @@
+module Ruby
+ class Float < Identifier
+ def value
+ token.to_f
+ end
+ end
+end
48 lib/ruby/hash.rb
@@ -0,0 +1,48 @@
+require 'ruby/assoc'
+
+module Ruby
+ class Hash < Node
+ child_accessor :assocs, :ldelim, :rdelim, :separators
+
+ def initialize(assocs = nil, ldelim = nil, rdelim = nil, separators = nil)
+ self.ldelim = ldelim
+ self.rdelim = rdelim
+ self.assocs = assocs || []
+ self.separators = separators || []
+ end
+
+ def [](key)
+ each { |assoc| return assoc.value if assoc.key.value == key } or nil
+ end
+
+ def []=(key, value)
+ value = from_native(value, nil, ' ') unless value.is_a?(Node)
+ if assoc = assocs.detect { |assoc| assoc.key.value == key }
+ assoc.value = value
+ else
+ # TODO never happens, fix positions
+ separators << Token.new(',')
+ assocs << Assoc.new(key, value)
+ self[key]
+ end
+ end
+
+ def delete(key)
+ delete_if { |assoc| assoc.key.value == key }
+ end
+
+ def value
+ code = to_ruby(false)
+ code = "{#{code}}" unless code =~ /^\s*{/
+ eval(code) rescue {}
+ end
+
+ def nodes
+ [ldelim, zip(separators), rdelim].flatten.compact
+ end
+
+ def method_missing(method, *args, &block)
+ @assocs.respond_to?(method) ? @assocs.send(method, *args, &block) : super
+ end
+ end
+end
6 lib/ruby/identifier.rb
@@ -0,0 +1,6 @@
+require 'ruby/token'
+
+module Ruby
+ class Identifier < Token
+ end
+end
7 lib/ruby/integer.rb
@@ -0,0 +1,7 @@
+module Ruby
+ class Integer < Identifier
+ def value
+ token.to_i
+ end
+ end
+end
29 lib/ruby/keyword.rb
@@ -0,0 +1,29 @@
+require 'ruby/node'
+
+module Ruby
+ class Keyword < Identifier
+ @@keywords = {
+ 'true' => true,
+ 'false' => false,
+ 'nil' => nil,
+ # 'and' => 'and',
+ # 'or' => 'or',
+ # 'not' => 'not',
+ # 'class' => 'class',
+ # 'module' => 'module',
+ # 'def' => 'def',
+ # 'do' => 'do',
+ # 'end' => 'end',
+ # 'if' => 'if',
+ # 'elsif' => 'elsif',
+ # 'else' => 'else',
+ # 'self' => 'self',
+ # '__FILE__' => '__FILE__',
+ # '__LINE__' => '__LINE__'
+ }
+
+ def value
+ @@keywords.has_key?(token) ? @@keywords[token] : token
+ end
+ end
+end
19 lib/ruby/method.rb
@@ -0,0 +1,19 @@
+require 'ruby/node'
+
+module Ruby
+ class Method < Node
+ child_accessor :identifier, :params, :body, :ldelim, :rdelim
+
+ def initialize(identifier, params, body, ldelim, rdelim)
+ self.identifier = identifier
+ self.params = params
+ self.body = body
+ self.ldelim = ldelim
+ self.rdelim = rdelim
+ end
+
+ def nodes
+ [ldelim, identifier, params, body, rdelim]
+ end
+ end
+end
138 lib/ruby/node.rb
@@ -0,0 +1,138 @@
+require 'core_ext/object/meta_class'
+require 'ruby/composite'
+
+module Ruby
+ class Node
+ class << self
+ def from_native(object, position = nil, whitespace = nil)
+ from_ruby(object.inspect, position, whitespace)
+ end
+
+ def from_ruby(src, position = nil, whitespace = nil)
+ Ripper::RubyBuilder.new(src).parse.statements.first.tap do |node|
+ node.position = position if position
+ node.whitespace = whitespace if whitespace
+ # p whitespace
+ # p node.whitespace
+ end
+ end
+ end
+
+ include Ansi
+ include Composite
+
+ attr_writer :whitespace
+
+ def initialize(position = nil, whitespace = nil)
+ self.position = position.dup if position
+ self.whitespace = whitespace if whitespace
+ end
+
+ def row
+ position[0]
+ end
+
+ def column
+ position[1]
+ end
+
+ def position
+ @position || nodes.each { |n| return n.position.dup if n } && nil # raise("position not set in #{self.class}")
+ end
+
+ def position=(position)
+ @position = position.dup
+ end
+
+ def whitespace
+ @whitespace || nodes.each { |n| return n.whitespace if n } && ''
+ end
+
+ def length(include_whitespace = false)
+ to_ruby(include_whitespace).length
+ end
+
+ def to_ruby(include_whitespace = false)
+ (include_whitespace ? whitespace : '') + nodes.map { |node| node.to_ruby(true) }.join.strip
+ end
+
+ def nodes
+ []
+ end
+
+ def filename
+ root? ? @filename : root.filename
+ end
+
+ def src_pos(include_whitespace = false)
+ line_pos(row) + column - (include_whitespace ? whitespace.length : 0)
+ end
+
+ def src(include_whitespace = false)
+ root? ? @src : root.src[src_pos(include_whitespace), length(include_whitespace)]
+ end
+
+ def lines
+ root.src.split("\n")
+ end
+
+ def line_pos(row)
+ (row > 0 ? lines[0..(row - 1)].inject(0) { |pos, line| pos + line.length + 1 } : 0)
+ end
+
+ # TODO what if a node spans multiple lines (like a block, method definition, ...)?
+ def line(options = {})
+ highlight = options.has_key?(:highlight) ? options[:highlight] : false
+ line = lines[row].dup
+ highlight ? line_head + ansi_format(to_ruby, [:red, :bold]) + line_tail : line
+ end
+
+ # excerpt from source, preceding and succeeding [Ruby.context_width] lines
+ def context(options = {})
+ (context_head(options) + [line(options)] + context_tail(options)).join("\n")
+ end
+
+ def context_head(options = {})
+ width = options.has_key?(:width) ? options[:width] : Ruby.context_width
+ min = [0, row - width].max
+ min < row ? lines[min..(row - 1)] : []
+ end
+
+ def context_tail(options = {})
+ width = options.has_key?(:width) ? options[:width] : Ruby.context_width
+ max = [row + width, lines.size].min
+ max > row ? lines[(row + 1)..max] : []
+ end
+
+ # all content that precedes the node in the first line of the node in source
+ def line_head
+ column == 0 ? '' : line[0..[column - 1, 0].max].to_s
+ end
+
+ # all content that succeeds the node in the last line of the node in source
+ def line_tail
+ line[(column + length)..-1].to_s
+ end
+
+ protected
+
+ def from_ruby(*args)
+ self.class.from_ruby(*args)
+ end
+
+ def from_native(*args)
+ self.class.from_native(*args)
+ end
+
+ def position_from(node, column_offset = 0)
+ @position = node.position.dup
+ @position[1] -= column_offset
+ end
+
+ def update_positions(row, column, offset_column)
+ pos = self.position
+ pos[1] += offset_column if pos && self.row == row && self.column > column
+ children.each { |c| c.send(:update_positions, row, column, offset_column) }
+ end
+ end
+end
45 lib/ruby/operators.rb
@@ -0,0 +1,45 @@
+require 'ruby/node'
+
+module Ruby
+ class Unary < Node
+ attr_accessor :operator, :operand
+
+ def initialize(operator, operand)
+ self.operator = operator
+ self.operand = operand
+ end
+
+ def nodes
+ [operator, operand]
+ end
+ end
+
+ class Binary < Node
+ attr_accessor :operator, :left, :right
+
+ def initialize(operator, left, right)
+ self.operator = operator
+ self.left = left
+ self.right = right
+ end
+
+ def nodes
+ [left, operator, right]
+ end
+ end
+
+ class IfOp < Node
+ attr_accessor :condition, :left, :right, :operators
+
+ def initialize(condition, left, right, operators)
+ self.condition = condition
+ self.left = left
+ self.right = right
+ self.operators = operators
+ end
+
+ def nodes
+ [[condition, left, right].zip(operators)].flatten.compact
+ end
+ end
+end
35 lib/ruby/params.rb
@@ -0,0 +1,35 @@
+require 'ruby/identifier'
+
+module Ruby
+ class ParamsList < Node # join with ArgsList?
+ child_accessor :params, :separators, :ldelim, :rdelim
+
+ def initialize(params, whitespace, ldelim, rdelim, separators)
+ self.ldelim = ldelim
+ self.rdelim = rdelim
+ self.separators = separators
+ self.params = params
+ end
+
+ def nodes
+ [ldelim, zip(separators), rdelim].flatten.compact
+ end
+
+ def method_missing(method, *args, &block)
+ @params.respond_to?(method) ? @params.send(method, *args, &block) : super
+ end
+ end
+
+ class RestParam < Identifier
+ child_accessor :ldelim
+
+ def initialize(token, position, ldelim)
+ self.ldelim = ldelim
+ super(token, ldelim.position)
+ end
+
+ def to_ruby(include_whitespace = false)
+ ldelim.to_ruby(include_whitespace) + super(true)
+ end
+ end
+end
43 lib/ruby/program.rb
@@ -0,0 +1,43 @@
+require 'ruby/node'
+
+module Ruby
+ class Program < Node
+ attr_accessor :src, :filename
+ child_accessor :statements
+
+ def initialize(src, filename, statements)
+ self.src = src
+ self.filename = filename
+ self.statements = filter_statements(statements).each { |s| s.parent = self }
+ super([0, 0])
+ end
+
+ def statement(&block)
+ @statements.each { |s| return s if yield(s) }
+ end
+
+ def replace_src(row, column, length, src)
+ # puts 'replace with: ' + src + " at: #{[row, column].inspect}, length: #{length}"
+ # puts '', 'before:', @src
+ @src[line_pos(row) + column, length] = src
+ save_src if filename
+ # puts '', 'after:', @src, ''
+ # puts '-------------------------------------------', ''
+ offset_column = src.length - length
+ update_positions(row, column + length, offset_column)
+ end
+
+ def save_src
+ File.open(filename, 'w+') { |f| f.write(src) }
+ end
+
+ def nodes
+ statements
+ end
+
+ # get rid of unsupported sexp nodes
+ def filter_statements(statements)
+ Array(statements).flatten.select { |s| s.kind_of?(Ruby::Node) }
+ end
+ end
+end
35 lib/ruby/string.rb
@@ -0,0 +1,35 @@
+require 'ruby/node'
+
+module Ruby
+ class String < Node
+ child_accessor :contents, :ldelim, :rdelim
+
+ def initialize(ldelim, rdelim = nil)
+ self.ldelim = ldelim
+ self.rdelim = rdelim
+ self.contents = []
+ end
+
+ def value
+ map { |content| content.value }.join
+ end
+
+ def src_pos(include_whitespace = false)
+ ldelim.src_pos(include_whitespace)
+ end
+
+ def nodes
+ [ldelim, contents, rdelim].flatten.compact
+ end
+
+ def method_missing(method, *args, &block)
+ contents.respond_to?(method) ? contents.send(method, *args, &block) : super
+ end
+ end
+
+ class StringContent < Token
+ def value
+ token
+ end
+ end
+end
38 lib/ruby/symbol.rb
@@ -0,0 +1,38 @@
+require 'ruby/identifier'
+
+module Ruby
+ class Symbol < Identifier
+ child_accessor :ldelim
+
+ def initialize(token, ldelim)
+ self.ldelim = ldelim
+ super(token)
+ end
+
+ def value
+ token.to_sym
+ end
+
+ def whitespace # FIXME remove these ...
+ ldelim.whitespace
+ end
+
+ def whitespace=(whitespace) # FIXME remove these ...
+ ldelim.whitespace = whitespace
+ end
+
+ def to_ruby(include_whitespace = false)
+ ldelim.to_ruby(include_whitespace) + token
+ end
+
+ def nodes
+ [ldelim]
+ end
+ end
+
+ class DynaSymbol < String
+ def value
+ super.to_sym
+ end
+ end
+end
16 lib/ruby/token.rb
@@ -0,0 +1,16 @@
+require 'ruby/node'
+
+module Ruby
+ class Token < Node
+ attr_accessor :token
+
+ def initialize(token, position = nil, whitespace = nil)
+ self.token = token
+ super(position, whitespace)
+ end
+
+ def to_ruby(include_whitespace = false)
+ (include_whitespace ? whitespace : '') + token.to_s
+ end
+ end
+end
16 lib/ruby/unsupported.rb
@@ -0,0 +1,16 @@
+require 'ruby/node'
+
+module Ruby
+ class Unsupported < Node
+ attr_accessor :token
+
+ def initialize(token, position = nil)
+ self.token = token
+ super(position)
+ end
+
+ def to_ruby(include_whitespace = false)
+ '(unsupported type)'
+ end
+ end
+end
1  test/all.rb
@@ -0,0 +1 @@
+Dir[File.dirname(__FILE__) + '/**/*_test.rb'].each { |file| require file }
106 test/fixtures/all.rb
@@ -0,0 +1,106 @@
+# occurences of \n will be replaced with actual newlines
+
+# numbers
+1
+1.1
+
+# symbols
+:a
+:"a"
+:"a \n b \n c"
+:'a'
+:'a \n b \n c'
+
+
+# strings
+
+"a"
+'a'
+%(a)
+%( a \n b \n c )
+%|a|
+%.a.
+
+
+# arrays
+
+[:a,:b,:c]
+[:a,:b, :c]
+[:a, :b,:c]
+[:a, :b, :c]
+[:a, :b, :c ]
+[ :a, :b, :c]
+[ :a, :b, :c ]
+
+[ \n :a, \n :b, \n :c \n ]
+
+%w(a b c)
+%w(a b c )
+%w( a b c)
+%w( a b c )
+
+# hashes
+
+{:a=>:a}
+{:a=>:a }
+{ :a=>:a}
+{:a => :a}
+{ :a=>:a }
+{:a=> :a }
+{ :a =>:a}
+{ :a => :a }
+
+{ \n :a => \n :a \n }
+
+# constants
+
+I18n
+
+# assignments
+
+a=b
+a,b=c
+a, b=c
+a, b=c,d
+a, b=c, d
+a, b=*c
+
+a = b
+a,b = c
+a, b = c
+a, b = c,d
+a, b = c, d
+a, b = *c
+
+a, \n b = \n c, \n d
+
+# calls
+
+t
+t()
+t(:a)
+t :a
+t :a
+
+I18n.t
+I18n.t()
+I18n.t(:a)
+
+t(:a,:b)
+t(:a, :b)
+t(:a ,:b)
+t(:a, :b )
+t(:a ,:b )
+t( :a, :b)
+t( :a ,:b)
+t( :a, :b )
+t( :a ,:b )
+
+t(:a, :b,&c)
+t(:a, :b, &c)
+
+t(:a, :b, :c => :c, &c)
+t(:a, :b, { :c => :c }, &c)
+
+t( \n:a, \n :b, \n :c => \n :c, \n &d)
+
15 test/fixtures/source_1.rb
@@ -0,0 +1,15 @@
+class Foo
+ def foo
+ t(:bar)
+ t(:"baaar")
+ t(:'baar', :scope => ['foo', :fooo], :default => 'bla')
+ t(:'foo.bar')
+ t("bar")
+ t('bar_1')
+ 1 + 1
+ t(1)
+ t(1.1)
+ t(1 + 1)
+ end
+ foo(:outside_)
+end
1  test/fixtures/source_2.rb
@@ -0,0 +1 @@
+t(:bar_2)
22 test/fixtures/template.html.erb
@@ -0,0 +1,22 @@
+<html>
+<ul>
+<% f.field_set do %>
+ <% column do %>
+ <% [:foo].each do |foo| %>
+ <li>
+
+ <span><%= t(:erb_1) %></span>
+ <%#= comment
+ comment
+ comment %>
+ </li>
+ <% end %>
+ <br>
+
+ <%- t(:erb_2) -%>
+
+ <% t(:'foo.erb_3') %> <% bar %>
+ <% end %>
+<% end %>
+</ul>
+</html>
1  test/ripper2ruby/all.rb
@@ -0,0 +1 @@
+Dir[File.dirname(__FILE__) + '/**/*_test.rb'].each { |file| require file }
49 test/ripper2ruby/builder_test.rb
@@ -0,0 +1,49 @@
+require File.dirname(__FILE__) + '/test_helper'
+
+class RipperToRubyBuilderTest < Test::Unit::TestCase
+ def setup
+ @builder = Ripper::RubyBuilder.new('')
+ @builder.push([:@lbrace])
+ @builder.push([:@comma])
+ @builder.push([:@comma])
+ @builder.push([:@rbrace])
+ end
+
+ define_method "test pop: pops off elements until it finds a token with the requested type" do
+ assert_equal :@rbrace, @builder.send(:pop, :@rbrace).first.type
+ assert !@builder.stack.empty?
+ assert @builder.send(:pop, :foo).empty?
+
+ commas = @builder.send(:pop, :@comma)
+ assert_equal 2, commas.length
+ assert_equal :@comma, commas[0].type
+ assert_equal :@comma, commas[1].type
+
+ assert_equal :@lbrace, @builder.send(:pop, :@lbrace).first.type
+ assert @builder.stack.empty?
+ assert @builder.send(:pop, :foo).empty?
+ end
+
+ define_method "test pop: leaves the stack untouched if it does not find a token with the requested type" do
+ assert @builder.send(:pop, :foo).empty?
+ assert !@builder.stack.empty?
+ assert !@builder.send(:pop, :@rbrace).empty?
+ end
+
+ define_method "test pop: ignoring types" do
+ @builder.send(:stack_ignore, :@rbrace) do
+ commas = @builder.send(:pop, :@comma)
+ assert_equal 2, commas.length
+ assert_equal :@comma, commas[0].type
+ assert_equal :@comma, commas[1].type
+ end
+ end
+
+ define_method "test pop: ignoring types nested" do
+ @builder.send(:stack_ignore, :@rbrace) do
+ @builder.send(:stack_ignore, :@comma) do
+ assert_equal :@lbrace, @builder.send(:pop, :@lbrace).first.type
+ end
+ end
+ end
+end
48 test/ripper2ruby/context_test.rb
@@ -0,0 +1,48 @@
+require File.dirname(__FILE__) + '/test_helper'
+
+class RipperRubyBuilderContextTest < Test::Unit::TestCase
+ def build(code)
+ Ripper::RubyBuilder.build(code)
+ end
+
+ def lines(range)
+ range.to_a.map { |i| " call_#{i}(:a)" }.join("\n")
+ end
+
+ define_method :"test context returns 5 lines" do
+ program = build(lines(1..10))
+ line = program.statements.select { |s| s.to_ruby == 'call_4(:a)' }[0]
+ assert_equal lines(2..6), line.context
+ end
+
+ define_method :"test context returns 3 lines when no preceeding lines present" do
+ program = build(lines(1..10))
+ line = program.statements.select { |s| s.to_ruby == 'call_1(:a)' }[0]
+ assert_equal lines(1..3), line.context
+ end
+
+ define_method :"test context returns 4 lines when only one preceeding line present" do
+ program = build(lines(1..10))
+ line = program.statements.select { |s| s.to_ruby == 'call_2(:a)' }[0]
+ assert_equal lines(1..4), line.context
+ end
+
+ define_method :"test context returns 4 lines when only one succeeding line present" do
+ program = build(lines(1..10))
+ line = program.statements.select { |s| s.to_ruby == 'call_9(:a)' }[0]
+ assert_equal lines(7..10), line.context
+ end
+
+ define_method :"test context returns 3 lines when no succeeding lines present" do
+ program = build(lines(1..10))
+ line = program.statements.select { |s| s.to_ruby == 'call_10(:a)' }[0]
+ assert_equal lines(8..10), line.context
+ end
+
+ define_method :"test context highlights elements" do
+ program = build(lines(1..10))
+ line = program.statements.select { |s| s.to_ruby == 'call_4(:a)' }[0]
+ assert_equal " \e[0;31;1mcall_4(:a)\e[0m", line.context(:highlight => true, :width => 0)
+ end
+
+end
29 test/ripper2ruby/erb_stripper_test.rb
@@ -0,0 +1,29 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+require 'ripper/erb/stripper'
+
+class ErbStripperTest < Test::Unit::TestCase
+ def test_sexp_filename
+ erb = File.read("#{File.dirname(__FILE__)}/../fixtures/template.html.erb")
+ ruby = Ripper::Erb::Stripper.new.to_ruby(erb)
+ expected = <<-src
+ f.field_set do
+ column do
+ [:foo].each do |foo|
+ t(:erb_1)
+ end
+ t(:erb_2)
+ t(:'foo.erb_3')
+ end
+ end
+ src
+ assert_equal erb.length, ruby.length
+ %w([:foo] erb_1 erb_2 foo.erb_3).each do |token|
+ assert ruby.index(token)
+ assert_equal erb.index(token), ruby.index(token)
+ end
+ expected.split("\n").each do |token|
+ assert ruby.index(token)
+ end
+ end
+end
1  test/ripper2ruby/nodes/all.rb
@@ -0,0 +1 @@
+Dir[File.dirname(__FILE__) + '/*_test.rb'].each { |file| require file }
192 test/ripper2ruby/nodes/args_test.rb
@@ -0,0 +1,192 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class RipperRubyBuilderArgsTest < Test::Unit::TestCase
+ include TestRubyBuilderHelper
+
+ define_method :"test call on no target, 3 arguments and parantheses" do
+ src = "t('a' , 'b', :c => :c)"
+ call = call(src)
+ args = call.arguments
+
+ assert args.root.is_a?(Ruby::Program)
+
+ assert_equal '(', args.ldelim.token
+ assert_equal ')', args.rdelim.token
+ assert_equal '', args.ldelim.whitespace
+
+ assert_equal 2, args.separators.length
+ assert_equal ',', args.separators[0].token
+ assert_equal ' ', args.separators[0].whitespace
+ assert_equal ',', args.separators[1].token
+ assert_equal '', args.separators[1].whitespace
+
+ assert_equal [0, 1], args.position
+ assert_equal 21, args.length
+ assert_equal src, call.to_ruby
+ end
+
+ define_method :'test method call: t("foo") (double-quoted string)' do
+ src = 't("foo")'
+ call = call(src)
+ args = call.arguments
+ string = args.first
+
+ assert_equal 'foo', string.first.value
+
+ assert_equal call, args.parent
+ assert_equal args, string.parent
+ assert_equal src, string.root.src
+ assert_equal src, call.to_ruby
+ end
+
+ define_method :"test method call: t('foo') (single-quoted string)" do
+ string = arguments("t('foo')").first
+ assert_equal 'foo', string.first.value
+ end
+
+ define_method :"test method call: t(:foo) (symbol)" do
+ symbol = arguments("t(:foo)").first
+ assert_equal :foo, symbol.value
+ end
+
+ define_method :'test method call: t(:"foo") (double-quoted symbol)' do
+ symbol = arguments('t(:"foo")').first
+ assert_equal :foo, symbol.value
+ end
+
+ define_method :"test method call: t(:'foo') (single-quoted symbol)" do
+ symbol = arguments("t(:'foo')").first
+ assert_equal :foo, symbol.value
+ end
+
+ define_method :"test method call: t 'foo' (string, no parantheses)" do
+ src = "t 'foo'"
+ call = call(src)
+ args = call.arguments
+ string = args.first
+
+ assert_equal 'foo', string.value
+
+ assert_equal Ruby::Call, args.parent.class
+ assert_equal args, string.parent
+ assert_equal src, call.to_ruby
+ end
+
+ define_method :"test method call: t :foo, :bar, :baz (3 symbols, no parantheses)" do
+ src = "t :foo, :bar, :baz"
+
+ call = call(src)
+ args = call.arguments
+ foo, bar, baz = args.args
+
+ assert_equal :foo, foo.value
+ assert_equal :bar, bar.value
+ assert_equal :baz, baz.value
+
+ assert_equal Ruby::Call, args.parent.class
+ assert_equal args, foo.parent
+ assert_equal args, bar.parent
+ assert_equal args, baz.parent
+
+ assert_equal 2, args.separators.size
+ assert_equal src, call.to_ruby
+ end
+
+ define_method :"test method call: t('foo', 'bar') (two strings)" do
+ args = arguments("t('foo', 'bar')")
+ assert_equal 'foo', args[0].value
+ assert_equal 'bar', args[1].value
+ end
+
+ define_method :"test method call: t(:foo => :bar, :baz => :buz) (bare hash)" do
+ src = "t(:foo => :bar, :baz => :buz)"
+ call = build(src).statements.first
+ hash = call.arguments.first
+
+ assert_equal :foo, hash.assocs[0].key.value
+ assert_equal :bar, hash.assocs[0].value.value
+ assert_equal :baz, hash.assocs[1].key.value
+ assert_equal :buz, hash.assocs[1].value.value
+
+ assert_equal src, call.to_ruby
+ end
+
+ define_method :"test method call: t({ :foo => :bar, :baz => :buz }) (hash)" do
+ src = "t({ :foo => :bar, :baz => :buz })"
+ call = build(src).statements.first
+ hash = call.arguments.first
+
+ assert_equal :foo, hash.assocs[0].key.value
+ assert_equal :bar, hash.assocs[0].value.value
+ assert_equal :baz, hash.assocs[1].key.value
+ assert_equal :buz, hash.assocs[1].value.value
+
+ assert_equal src, call.to_ruby
+ end
+
+ define_method :"test method call: t([:foo, :bar]) (array)" do
+ src = "t([:foo, :bar, :baz])"
+
+ call = build(src).statements.first
+ array = call.arguments.first
+
+ assert_equal :foo, array[0].value
+ assert_equal :bar, array[1].value
+ assert_equal :baz, array[2].value
+
+ assert_equal src, call.to_ruby
+ end
+
+ define_method :"test method call: t(nil) (keyword)" do
+ src = "t(nil)"
+ call = build(src).statements.first
+ kw = call.arguments.first
+
+ assert_equal nil, kw.value
+ assert_equal src, call.to_ruby
+ end
+
+ define_method :"test method call: t(1) (integer)" do
+ src = "t(1)"
+ call = build(src).statements.first
+ integer = call.arguments.first
+
+ assert_equal 1, integer.value
+ assert_equal src, call.to_ruby
+ end
+
+ define_method :"test method call: t(1) (float)" do
+ src = "t(1.1)"
+ call = build(src).statements.first
+ float = call.arguments.first
+
+ assert_equal 1.1, float.value
+ assert_equal src, call.to_ruby
+ end
+
+ define_method :"test: replace argument" do
+ call = call("t(:foo, :bar)")
+ args = call.arguments
+ foo, bar = args
+
+ baz = Ruby::Symbol.from_native(:baz)
+ args[0] = baz
+
+ assert_equal args, baz.parent
+ assert_equal [0, 2], baz.position
+ end
+
+ # TODO
+ #
+ # define_method :"test: args with a star" do
+ # src = "t(*a.b)"
+ # call = build(src).statements.first
+ # assert_equal src, call.to_ruby
+ # end
+ #
+ # define_method :"test: args with a star and block" do
+ # src = "t(*a.b { |c| c.d })"
+ # call = build(src).statements.first
+ # assert_equal src, call.to_ruby
+ # end
+end
99 test/ripper2ruby/nodes/array_test.rb
@@ -0,0 +1,99 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+class RipperToRubyArrayTest < Test::Unit::TestCase
+ include TestRubyBuilderHelper
+
+ define_method :'test an array: [:foo, :bar]' do
+ src = '[:foo, :bar]'
+ array = array(src)
+
+ assert_equal Ruby::Array, array.class
+ assert_equal :foo, array.first.value
+
+ assert array.parent.is_a?(Ruby::Program)
+ assert_equal array, array.first.parent
+
+ assert_equal src, array.root.src
+ assert_equal src, array.first.root.src
+ assert_equal src, array.to_ruby
+
+ assert_equal [0, 0], array.position
+ assert_equal 0, array.row
+ assert_equal 0, array.column
+
+ assert_equal [0, 1], array[0].position
+ assert_equal 0, array[0].row
+ assert_equal 1, array[0].column
+
+ assert_equal [0, 7], array[1].position
+ assert_equal 0, array[1].row
+ assert_equal 7, array[1].column
+ end
+
+ define_method :'test a wordlist array %w(foo bar)' do
+ src = '%w(foo bar)'
+ array = array(src)
+
+ assert_equal Ruby::Array, array.class
+ assert_equal 'foo', array[0].value
+ assert_equal 'bar', array[1].value
+
+ assert array.parent.is_a?(Ruby::Program)
+ assert_equal array, array.first.parent
+
+ assert_equal src, array.root.src
+ assert_equal src, array.first.root.src
+ assert_equal src, array.to_ruby
+
+ assert_equal [0, 0], array.position
+ assert_equal 0, array.row
+ assert_equal 0, array.column
+ assert_equal 11, array.length
+ end
+