Skip to content

Parse tests with prism #51006

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 9, 2024
Merged

Parse tests with prism #51006

merged 2 commits into from
Feb 9, 2024

Conversation

kddnewton
Copy link
Contributor

Motivation / Background

This is a refactor PR to change TestParser to parse using the prism parser if it is available instead of the ripper library.

Detail

Prism provides a simpler interface with consistent field names, so it should be easier to maintain going forward. It also has a guaranteed stable API, as opposed to ripper which can change between patch versions of Ruby. It also results in less code.

Additional information

I benchmarked this change, and it appears to have no visible difference (it's within the margin of error). My benchmark script is below.

Benchmark
# frozen_string_literal: true

require "active_support/test_case"
require "prism"
require "ripper"
require "benchmark/ips"

class PrismParser
  def self.definition_for(method)
    filepath, start_line = method.source_location
    queue = [Prism.parse_file(filepath).value]

    while (node = queue.shift)
      case node.type
      when :def_node
        if node.name.start_with?("test") && node.location.start_line == start_line
          return [filepath, start_line..node.location.end_line]
        end
      when :call_node
        if node.name == :test && node.location.start_line == start_line
          return [filepath, start_line..node.location.end_line]
        end
      end

      queue.concat(node.compact_child_nodes)
    end

    nil
  end
end

class RipperParser < Ripper
  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

  def on_def(begin_line, *)
    @begins_to_ends[begin_line] = lineno
  end

  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

class ExampleTest < ActiveSupport::TestCase
  def test_method
    assert true


  end

  def test_oneline; assert true; end

  test "declarative" do
    assert true
  end

  test("declarative w/parens") do
    assert true

  end

  self.test "declarative explicit receiver" do
    assert true
  end

  test("declarative oneline") { assert true }

  test("declarative oneline do") do assert true end

  test("declarative multiline w/ braces") {
    assert true
    refute false
  }
end

methods = ExampleTest.instance_methods.grep(/^test_/).map { |method| ExampleTest.instance_method(method) }

Benchmark.ips do |x|
  x.report("Prism") { methods.each { |method| PrismParser.definition_for(method) } }
  x.report("Ripper") { methods.each { |method| RipperParser.definition_for(method) } }
  x.compare!
end

Checklist

Before submitting the PR make sure the following are checked:

  • This Pull Request is related to one change. Changes that are unrelated should be opened in separate PRs.
  • Commit message has a detailed description of what changed and why. If this PR fixes a related issue include it in the commit message. Ex: [Fix #issue-number]
  • Tests are added or updated if you fix a bug or add a feature.
  • CHANGELOG files are updated for the changed libraries if there is a behavior change or additional feature. Minor bug fixes and documentation changes should not be included.

This changes TestParser to parse with prism instead of ripper if it
is available for the current version of Ruby. It's within the margin
for the speed, and its significantly less code that is easier to
read and should be easier to maintain.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants