# frozen_string_literal: false require 'test/unit' require 'tempfile' require 'pp' 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("< do a STR (SCOPE@1:0-2:3 tbl: [] args: nil body: (LAMBDA@1:0-2:3 (SCOPE@1:2-2:3 tbl: [] args: (ARGS@1:2-1:2 pre_num: 0 pre_init: nil opt: nil first_post: nil post_num: 0 post_init: nil rest: nil kw: nil kwrest: nil block: nil) body: (VCALL@2:2-2:3 :a)))) EXP end def test_error_tolerant_treat_end_as_keyword_based_on_indent assert_error_tolerant(<<~STR, <<~EXP) module Z class Foo foo. end def bar end end STR (SCOPE@1:0-8:3 tbl: [] args: nil body: (MODULE@1:0-8:3 (COLON2@1:7-1:8 nil :Z) (SCOPE@1:0-8:3 tbl: [] args: nil body: (BLOCK@1:8-7:5 (BEGIN@1:8-1:8 nil) (CLASS@2:2-4:5 (COLON2@2:8-2:11 nil :Foo) nil (SCOPE@2:2-4:5 tbl: [] args: nil body: (BLOCK@2:11-4:5 (BEGIN@2:11-2:11 nil) (ERROR@3:4-4:5)))) (DEFN@6:2-7:5 mid: :bar body: (SCOPE@6:2-7:5 tbl: [] args: (ARGS@6:9-6:9 pre_num: 0 pre_init: nil opt: nil first_post: nil post_num: 0 post_init: nil rest: nil kw: nil kwrest: nil block: nil) body: nil)))))) EXP end def test_error_tolerant_expr_value_can_be_error assert_error_tolerant(<<~STR, <<~EXP) def m if end STR (SCOPE@1:0-3:3 tbl: [] args: nil body: (DEFN@1:0-3:3 mid: :m body: (SCOPE@1:0-3:3 tbl: [] args: (ARGS@1:5-1:5 pre_num: 0 pre_init: nil opt: nil first_post: nil post_num: 0 post_init: nil rest: nil kw: nil kwrest: nil block: nil) body: (IF@2:2-3:3 (ERROR@3:0-3:3) nil nil)))) EXP end def assert_error_tolerant(src, expected) begin verbose_bak, $VERBOSE = $VERBOSE, false node = RubyVM::AbstractSyntaxTree.parse(src, error_tolerant: true) ensure $VERBOSE = verbose_bak end assert_nil($!) str = "" PP.pp(node, str, 80) assert_equal(expected, str) end end