# 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("<