lib/rails/test_unit/test_parser.rb



# frozen_string_literal: true

require "ripper"

module Rails
  module TestUnit
    # Parse a test file to extract the line ranges of all tests in both
    # method-style (def test_foo) and declarative-style (test "foo" do)
    class TestParser < Ripper # :nodoc:
      # Helper to translate a method object into the path and line range where
      # the method was defined.
      def self.definition_for(method_obj)
        path, begin_line = method_obj.source_location
        begins_to_ends = new(File.read(path), path).parse
        return unless end_line = begins_to_ends[begin_line]
        [path, (begin_line..end_line)]
      end

      def initialize(*)
        # A hash mapping the 1-indexed line numbers that tests start on to where they end.
        @begins_to_ends = {}
        super
      end

      def parse
        super
        @begins_to_ends
      end

      # method test e.g. `def test_some_description`
      # This event's first argument gets the `ident` node containing the method
      # name, which we have overridden to return the line number of the ident
      # instead.
      def on_def(begin_line, *)
        @begins_to_ends[begin_line] = lineno
      end

      # Everything past this point is to support declarative tests, which
      # require more work to get right because of the many different ways
      # methods can be invoked in ruby, all of which are parsed differently.
      #
      # The approach is just to store the current line number when the
      # "test" method is called and pass it up the tree so it's available at
      # the point when we also know the line where the associated block ends.

      def on_method_add_block(begin_line, end_line)
        if begin_line && end_line
          @begins_to_ends[begin_line] = end_line
        end
      end

      def on_command_call(*, begin_lineno, _args)
        begin_lineno
      end

      def first_arg(arg, *)
        arg
      end

      def just_lineno(*)
        lineno
      end

      alias on_method_add_arg first_arg
      alias on_command first_arg
      alias on_stmts_add first_arg
      alias on_arg_paren first_arg
      alias on_bodystmt first_arg

      alias on_ident just_lineno
      alias on_do_block just_lineno
      alias on_stmts_new just_lineno
      alias on_brace_block just_lineno

      def on_args_new
        []
      end

      def on_args_add(parts, part)
        parts << part
      end

      def on_args_add_block(args, *rest)
        args.first
      end
    end
  end
end