aboutsummaryrefslogtreecommitdiffstats
path: root/test/yarp/newline_test.rb
blob: 80f6329d94d6c234b0caae7337241303565b735d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# frozen_string_literal: true

require "yarp_test_helper"

return unless defined?(RubyVM::InstructionSequence)

# It is useful to have a diff even if the strings to compare are big
# However, ruby/ruby does not have a version of Test::Unit with access to
# max_diff_target_string_size
if defined?(Test::Unit::Assertions::AssertionMessage)
  Test::Unit::Assertions::AssertionMessage.max_diff_target_string_size = 5000
end

class NewlineTest < Test::Unit::TestCase
  class NewlineVisitor < YARP::Visitor
    attr_reader :source, :newlines

    def initialize(source)
      @source = source
      @newlines = []
    end

    def visit(node)
      newlines << source.line(node.location.start_offset) if node&.newline?
      super(node)
    end
  end

  base = File.dirname(__dir__)
  Dir["{lib,test}/**/*.rb", base: base].each do |relative|
    define_method("test_newline_flags_#{relative}") do
      assert_newlines(base, relative)
    end
  end

  private

  def assert_newlines(base, relative)
    filepath = File.join(base, relative)
    source = File.read(filepath, binmode: true, external_encoding: Encoding::UTF_8)
    expected = rubyvm_lines(source)

    result = YARP.parse_file(filepath)
    assert_empty result.errors

    result.mark_newlines
    visitor = NewlineVisitor.new(result.source)

    result.value.accept(visitor)
    actual = visitor.newlines

    source.each_line.with_index(1) do |line, line_number|
      # Lines like `while (foo = bar)` result in two line flags in the bytecode
      # but only one newline flag in the AST. We need to remove the extra line
      # flag from the bytecode to make the test pass.
      if line.match?(/while \(/)
        index = expected.index(line_number)
        expected.delete_at(index) if index
      end

      # Lines like `foo =` where the value is on the next line result in another
      # line flag in the bytecode but only one newline flag in the AST.
      if line.match?(/^\s+\w+ =$/)
        if source.lines[line_number].match?(/^\s+case/)
          actual[actual.index(line_number)] += 1
        else
          actual.delete_at(actual.index(line_number))
        end
      end

      if line.match?(/^\s+\w+ = \[$/)
        if !expected.include?(line_number) && !expected.include?(line_number + 2)
          actual[actual.index(line_number)] += 1
        end
      end
    end

    assert_equal expected, actual
  end

  def ignore_warnings
    previous_verbosity = $VERBOSE
    $VERBOSE = nil
    yield
  ensure
    $VERBOSE = previous_verbosity
  end

  def rubyvm_lines(source)
    queue = [ignore_warnings { RubyVM::InstructionSequence.compile(source) }]
    lines = []

    while iseq = queue.shift
      lines.concat(iseq.trace_points.filter_map { |line, event| line if event == :line })
      iseq.each_child { |insn| queue << insn unless insn.label.start_with?("ensure in ") }
    end

    lines.sort
  end
end