mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
cd5cafa4a3
Do not override the input string encoding at the time of preparation, the source encoding is not determined from the input yet.
562 lines
16 KiB
Ruby
562 lines
16 KiB
Ruby
# 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
|
|
pend if ENV['RUBY_ISEQ_DUMP_DEBUG'] # TODO
|
|
|
|
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
|
|
pend if ENV['RUBY_ISEQ_DUMP_DEBUG'] # TODO
|
|
|
|
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
|