diff options
Diffstat (limited to 'test/ruby/test_ast.rb')
| -rw-r--r-- | test/ruby/test_ast.rb | 558 |
1 files changed, 558 insertions, 0 deletions
diff --git a/test/ruby/test_ast.rb b/test/ruby/test_ast.rb new file mode 100644 index 0000000000..cd7299f200 --- /dev/null +++ b/test/ruby/test_ast.rb @@ -0,0 +1,558 @@ +# frozen_string_literal: false +require 'test/unit' +require 'tempfile' + +class RubyVM + module AbstractSyntaxTree + class Node + class CodePosition + include Comparable + attr_reader :lineno, :column + def initialize(lineno, column) + @lineno = lineno + @column = column + end + + def <=>(other) + case + when lineno < other.lineno + -1 + when lineno == other.lineno + column <=> other.column + when lineno > other.lineno + 1 + end + end + end + + def beg_pos + CodePosition.new(first_lineno, first_column) + end + + def end_pos + CodePosition.new(last_lineno, last_column) + end + + alias to_s inspect + end + end +end + +class TestAst < Test::Unit::TestCase + class Helper + attr_reader :errors + + def initialize(path, src: nil) + @path = path + @errors = [] + @debug = false + @ast = RubyVM::AbstractSyntaxTree.parse(src) if src + end + + def validate_range + @errors = [] + validate_range0(ast) + + @errors.empty? + end + + def validate_not_cared + @errors = [] + validate_not_cared0(ast) + + @errors.empty? + end + + def ast + return @ast if defined?(@ast) + @ast = RubyVM::AbstractSyntaxTree.parse_file(@path) + end + + private + + def validate_range0(node) + beg_pos, end_pos = node.beg_pos, node.end_pos + children = node.children.grep(RubyVM::AbstractSyntaxTree::Node) + + return true if children.empty? + # These NODE_D* has NODE_LIST as nd_next->nd_next whose last locations + # we can not update when item is appended. + return true if [:DSTR, :DXSTR, :DREGX, :DSYM].include? node.type + + min = children.map(&:beg_pos).min + max = children.map(&:end_pos).max + + unless beg_pos <= min + @errors << { type: :min_validation_error, min: min, beg_pos: beg_pos, node: node } + end + + unless max <= end_pos + @errors << { type: :max_validation_error, max: max, end_pos: end_pos, node: node } + end + + p "#{node} => #{children}" if @debug + + children.each do |child| + p child if @debug + validate_range0(child) + end + end + + def validate_not_cared0(node) + beg_pos, end_pos = node.beg_pos, node.end_pos + children = node.children.grep(RubyVM::AbstractSyntaxTree::Node) + + @errors << { type: :first_lineno, node: node } if beg_pos.lineno == 0 + @errors << { type: :first_column, node: node } if beg_pos.column == -1 + @errors << { type: :last_lineno, node: node } if end_pos.lineno == 0 + @errors << { type: :last_column, node: node } if end_pos.column == -1 + + children.each {|c| validate_not_cared0(c) } + end + end + + SRCDIR = File.expand_path("../../..", __FILE__) + + Dir.glob("test/**/*.rb", base: SRCDIR).each do |path| + define_method("test_ranges:#{path}") do + helper = Helper.new("#{SRCDIR}/#{path}") + helper.validate_range + + assert_equal([], helper.errors) + end + end + + Dir.glob("test/**/*.rb", base: SRCDIR).each do |path| + define_method("test_not_cared:#{path}") do + helper = Helper.new("#{SRCDIR}/#{path}") + helper.validate_not_cared + + assert_equal([], helper.errors) + end + end + + private def parse(src) + EnvUtil.suppress_warning { + RubyVM::AbstractSyntaxTree.parse(src) + } + end + + def test_allocate + assert_raise(TypeError) {RubyVM::AbstractSyntaxTree::Node.allocate} + end + + def test_parse_argument_error + assert_raise(TypeError) {RubyVM::AbstractSyntaxTree.parse(0)} + assert_raise(TypeError) {RubyVM::AbstractSyntaxTree.parse(nil)} + assert_raise(TypeError) {RubyVM::AbstractSyntaxTree.parse(false)} + assert_raise(TypeError) {RubyVM::AbstractSyntaxTree.parse(true)} + assert_raise(TypeError) {RubyVM::AbstractSyntaxTree.parse(:foo)} + end + + def test_column_with_long_heredoc_identifier + term = "A"*257 + ast = parse("<<-#{term}\n""ddddddd\n#{term}\n") + node = ast.children[2] + assert_equal(:STR, node.type) + assert_equal(0, node.first_column) + end + + def test_column_of_heredoc + node = parse("<<-SRC\nddddddd\nSRC\n").children[2] + assert_equal(:STR, node.type) + assert_equal(0, node.first_column) + assert_equal(6, node.last_column) + + node = parse("<<SRC\nddddddd\nSRC\n").children[2] + assert_equal(:STR, node.type) + assert_equal(0, node.first_column) + assert_equal(5, node.last_column) + end + + def test_parse_raises_syntax_error + assert_raise_with_message(SyntaxError, /\bend\b/) do + RubyVM::AbstractSyntaxTree.parse("end") + end + end + + def test_parse_file_raises_syntax_error + Tempfile.create(%w"test_ast .rb") do |f| + f.puts "end" + f.close + assert_raise_with_message(SyntaxError, /\bend\b/) do + RubyVM::AbstractSyntaxTree.parse_file(f.path) + end + end + end + + def test_of_proc_and_method + proc = Proc.new { 1 + 2 } + method = self.method(__method__) + + node_proc = RubyVM::AbstractSyntaxTree.of(proc) + node_method = RubyVM::AbstractSyntaxTree.of(method) + + assert_instance_of(RubyVM::AbstractSyntaxTree::Node, node_proc) + assert_instance_of(RubyVM::AbstractSyntaxTree::Node, node_method) + + Tempfile.create(%w"test_of .rb") do |tmp| + tmp.print "#{<<-"begin;"}\n#{<<-'end;'}" + begin; + SCRIPT_LINES__ = {} + assert_instance_of(RubyVM::AbstractSyntaxTree::Node, RubyVM::AbstractSyntaxTree.of(proc {|x| x})) + end; + tmp.close + assert_separately(["-", tmp.path], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + load ARGV[0] + assert_empty(SCRIPT_LINES__) + end; + end + end + + def sample_backtrace_location + [caller_locations(0).first, __LINE__] + end + + def test_of_backtrace_location + backtrace_location, lineno = sample_backtrace_location + node = RubyVM::AbstractSyntaxTree.of(backtrace_location) + assert_instance_of(RubyVM::AbstractSyntaxTree::Node, node) + assert_equal(lineno, node.first_lineno) + end + + def test_of_error + assert_raise(TypeError) { RubyVM::AbstractSyntaxTree.of("1 + 2") } + end + + def test_of_proc_and_method_under_eval + keep_script_lines_back = RubyVM.keep_script_lines + RubyVM.keep_script_lines = false + + method = self.method(eval("def example_method_#{$$}; end")) + assert_raise(ArgumentError) { RubyVM::AbstractSyntaxTree.of(method) } + + method = self.method(eval("def self.example_singleton_method_#{$$}; end")) + assert_raise(ArgumentError) { RubyVM::AbstractSyntaxTree.of(method) } + + method = eval("proc{}") + assert_raise(ArgumentError) { RubyVM::AbstractSyntaxTree.of(method) } + + method = self.method(eval("singleton_class.define_method(:example_define_method_#{$$}){}")) + assert_raise(ArgumentError) { RubyVM::AbstractSyntaxTree.of(method) } + + method = self.method(eval("define_singleton_method(:example_dsm_#{$$}){}")) + assert_raise(ArgumentError) { RubyVM::AbstractSyntaxTree.of(method) } + + method = eval("Class.new{def example_method; end}.instance_method(:example_method)") + assert_raise(ArgumentError) { RubyVM::AbstractSyntaxTree.of(method) } + + method = eval("Class.new{def example_method; end}.instance_method(:example_method)") + assert_raise(ArgumentError) { RubyVM::AbstractSyntaxTree.of(method) } + + ensure + RubyVM.keep_script_lines = keep_script_lines_back + end + + def test_of_proc_and_method_under_eval_with_keep_script_lines + keep_script_lines_back = RubyVM.keep_script_lines + RubyVM.keep_script_lines = true + + method = self.method(eval("def example_method_#{$$}_with_keep_script_lines; end")) + assert_instance_of(RubyVM::AbstractSyntaxTree::Node, RubyVM::AbstractSyntaxTree.of(method)) + + method = self.method(eval("def self.example_singleton_method_#{$$}_with_keep_script_lines; end")) + assert_instance_of(RubyVM::AbstractSyntaxTree::Node, RubyVM::AbstractSyntaxTree.of(method)) + + method = eval("proc{}") + assert_instance_of(RubyVM::AbstractSyntaxTree::Node, RubyVM::AbstractSyntaxTree.of(method)) + + method = self.method(eval("singleton_class.define_method(:example_define_method_#{$$}_with_keep_script_lines){}")) + assert_instance_of(RubyVM::AbstractSyntaxTree::Node, RubyVM::AbstractSyntaxTree.of(method)) + + method = self.method(eval("define_singleton_method(:example_dsm_#{$$}_with_keep_script_lines){}")) + assert_instance_of(RubyVM::AbstractSyntaxTree::Node, RubyVM::AbstractSyntaxTree.of(method)) + + method = eval("Class.new{def example_method_with_keep_script_lines; end}.instance_method(:example_method_with_keep_script_lines)") + assert_instance_of(RubyVM::AbstractSyntaxTree::Node, RubyVM::AbstractSyntaxTree.of(method)) + + method = eval("Class.new{def example_method_with_keep_script_lines; end}.instance_method(:example_method_with_keep_script_lines)") + assert_instance_of(RubyVM::AbstractSyntaxTree::Node, RubyVM::AbstractSyntaxTree.of(method)) + + ensure + RubyVM.keep_script_lines = keep_script_lines_back + end + + def test_of_backtrace_location_under_eval + keep_script_lines_back = RubyVM.keep_script_lines + RubyVM.keep_script_lines = false + + m = Module.new do + eval(<<-END, nil, __FILE__, __LINE__) + def self.sample_backtrace_location + caller_locations(0).first + end + END + end + backtrace_location = m.sample_backtrace_location + assert_raise(ArgumentError) { RubyVM::AbstractSyntaxTree.of(backtrace_location) } + + ensure + RubyVM.keep_script_lines = keep_script_lines_back + end + + def test_of_backtrace_location_under_eval_with_keep_script_lines + keep_script_lines_back = RubyVM.keep_script_lines + RubyVM.keep_script_lines = true + + m = Module.new do + eval(<<-END, nil, __FILE__, __LINE__) + def self.sample_backtrace_location + caller_locations(0).first + end + END + end + backtrace_location = m.sample_backtrace_location + node = RubyVM::AbstractSyntaxTree.of(backtrace_location) + assert_instance_of(RubyVM::AbstractSyntaxTree::Node, node) + assert_equal(2, node.first_lineno) + + ensure + RubyVM.keep_script_lines = keep_script_lines_back + end + + def test_of_c_method + c = Class.new { attr_reader :foo } + assert_nil(RubyVM::AbstractSyntaxTree.of(c.instance_method(:foo))) + end + + def test_scope_local_variables + node = RubyVM::AbstractSyntaxTree.parse("_x = 0") + lv, _, body = *node.children + assert_equal([:_x], lv) + assert_equal(:LASGN, body.type) + end + + def test_call + node = RubyVM::AbstractSyntaxTree.parse("nil.foo") + _, _, body = *node.children + assert_equal(:CALL, body.type) + recv, mid, args = body.children + assert_equal(:NIL, recv.type) + assert_equal(:foo, mid) + assert_nil(args) + end + + def test_fcall + node = RubyVM::AbstractSyntaxTree.parse("foo()") + _, _, body = *node.children + assert_equal(:FCALL, body.type) + mid, args = body.children + assert_equal(:foo, mid) + assert_nil(args) + end + + def test_vcall + node = RubyVM::AbstractSyntaxTree.parse("foo") + _, _, body = *node.children + assert_equal(:VCALL, body.type) + mid, args = body.children + assert_equal(:foo, mid) + assert_nil(args) + end + + def test_defn + node = RubyVM::AbstractSyntaxTree.parse("def a; end") + _, _, body = *node.children + assert_equal(:DEFN, body.type) + mid, defn = body.children + assert_equal(:a, mid) + assert_equal(:SCOPE, defn.type) + _, args, = defn.children + assert_equal(:ARGS, args.type) + end + + def test_defn_endless + node = RubyVM::AbstractSyntaxTree.parse("def a = nil") + _, _, body = *node.children + assert_equal(:DEFN, body.type) + mid, defn = body.children + assert_equal(:a, mid) + assert_equal(:SCOPE, defn.type) + _, args, = defn.children + assert_equal(:ARGS, args.type) + end + + def test_defs + node = RubyVM::AbstractSyntaxTree.parse("def a.b; end") + _, _, body = *node.children + assert_equal(:DEFS, body.type) + recv, mid, defn = body.children + assert_equal(:VCALL, recv.type) + assert_equal(:b, mid) + assert_equal(:SCOPE, defn.type) + _, args, = defn.children + assert_equal(:ARGS, args.type) + end + + def test_defs_endless + node = RubyVM::AbstractSyntaxTree.parse("def a.b = nil") + _, _, body = *node.children + assert_equal(:DEFS, body.type) + recv, mid, defn = body.children + assert_equal(:VCALL, recv.type) + assert_equal(:b, mid) + assert_equal(:SCOPE, defn.type) + _, args, = defn.children + assert_equal(:ARGS, args.type) + end + + def test_dstr + node = parse('"foo#{1}bar"') + _, _, body = *node.children + assert_equal(:DSTR, body.type) + head, body = body.children + assert_equal("foo", head) + assert_equal(:EVSTR, body.type) + body, = body.children + assert_equal(:LIT, body.type) + assert_equal([1], body.children) + end + + def test_while + node = RubyVM::AbstractSyntaxTree.parse('1 while qux') + _, _, body = *node.children + assert_equal(:WHILE, body.type) + type1 = body.children[2] + node = RubyVM::AbstractSyntaxTree.parse('begin 1 end while qux') + _, _, body = *node.children + assert_equal(:WHILE, body.type) + type2 = body.children[2] + assert_not_equal(type1, type2) + end + + def test_until + node = RubyVM::AbstractSyntaxTree.parse('1 until qux') + _, _, body = *node.children + assert_equal(:UNTIL, body.type) + type1 = body.children[2] + node = RubyVM::AbstractSyntaxTree.parse('begin 1 end until qux') + _, _, body = *node.children + assert_equal(:UNTIL, body.type) + type2 = body.children[2] + assert_not_equal(type1, type2) + end + + def test_keyword_rest + kwrest = lambda do |arg_str| + node = RubyVM::AbstractSyntaxTree.parse("def a(#{arg_str}) end") + node = node.children.last.children.last.children[1].children[-2] + node ? node.children : node + end + + assert_equal(nil, kwrest.call('')) + assert_equal([nil], kwrest.call('**')) + assert_equal(false, kwrest.call('**nil')) + assert_equal([:a], kwrest.call('**a')) + end + + def test_ranges_numbered_parameter + helper = Helper.new(__FILE__, src: "1.times {_1}") + helper.validate_range + assert_equal([], helper.errors) + end + + def test_op_asgn2 + node = RubyVM::AbstractSyntaxTree.parse("struct.field += foo") + _, _, body = *node.children + assert_equal(:OP_ASGN2, body.type) + recv, _, mid, op, value = body.children + assert_equal(:VCALL, recv.type) + assert_equal(:field, mid) + assert_equal(:+, op) + assert_equal(:VCALL, value.type) + end + + def test_args + rest = 6 + node = RubyVM::AbstractSyntaxTree.parse("proc { |a| }") + _, args = *node.children.last.children[1].children + assert_equal(nil, args.children[rest]) + + node = RubyVM::AbstractSyntaxTree.parse("proc { |a,| }") + _, args = *node.children.last.children[1].children + assert_equal(:NODE_SPECIAL_EXCESSIVE_COMMA, args.children[rest]) + + node = RubyVM::AbstractSyntaxTree.parse("proc { |*a| }") + _, args = *node.children.last.children[1].children + assert_equal(:a, args.children[rest]) + end + + def test_keep_script_lines_for_parse + node = RubyVM::AbstractSyntaxTree.parse(<<~END, keep_script_lines: true) +1.times do + 2.times do + end +end +__END__ +dummy + END + + expected = [ + "1.times do\n", + " 2.times do\n", + " end\n", + "end\n", + "__END__\n", + ] + assert_equal(expected, node.script_lines) + + expected = + "1.times do\n" + + " 2.times do\n" + + " end\n" + + "end" + assert_equal(expected, node.source) + + expected = + "do\n" + + " 2.times do\n" + + " end\n" + + "end" + assert_equal(expected, node.children.last.children.last.source) + + expected = + "2.times do\n" + + " end" + assert_equal(expected, node.children.last.children.last.children.last.source) + end + + def test_keep_script_lines_for_of + proc = Proc.new { 1 + 2 } + method = self.method(__method__) + + node_proc = RubyVM::AbstractSyntaxTree.of(proc, keep_script_lines: true) + node_method = RubyVM::AbstractSyntaxTree.of(method, keep_script_lines: true) + + assert_equal("{ 1 + 2 }", node_proc.source) + assert_equal("def test_keep_script_lines_for_of\n", node_method.source.lines.first) + end + + def test_encoding_with_keep_script_lines + enc = Encoding::EUC_JP + code = "__ENCODING__".encode(enc) + + assert_equal(enc, eval(code)) + + node = RubyVM::AbstractSyntaxTree.parse(code, keep_script_lines: false) + assert_equal(enc, node.children[2].children[0]) + + node = RubyVM::AbstractSyntaxTree.parse(code, keep_script_lines: true) + assert_equal(enc, node.children[2].children[0]) + end + + def test_e_option + assert_in_out_err(["-e", "def foo; end; pp RubyVM::AbstractSyntaxTree.of(method(:foo)).type"], + "", [":SCOPE"], []) + end +end |
