Skip to content

lambda nested in array of hashes causes Proc#source to raise MethodSource::SourceNotFoundError #76

@dmlary

Description

@dmlary

Issue

Ran into this issue while writing parameterized tests that include an optional block argument. Simplest implementation of the problem:

require "method_source"

a = [
  {block: ->(e) { e << %i[a c] }},
]

puts a.first[:block].source

Output:

/Users/me/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/method_source-1.0.0/lib/method_source.rb:29:in `rescue in source_helper': Could not parse source for #<Proc:0x0000000100999ef8 ./source.rb:4 (lambda)>: (eval):2: syntax error, unexpected ',', expecting end-of-input (MethodSource::SourceNotFoundError)
...block: ->(e) { e << %i[a c] }},
...                              ^
	from /Users/me/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/method_source-1.0.0/lib/method_source.rb:23:in `source_helper'
	from /Users/me/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/method_source-1.0.0/lib/method_source.rb:110:in `source'
	from ./source.rb:7:in `<main>'
/Users/me/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/method_source-1.0.0/lib/method_source/code_helpers.rb:71:in `eval': (eval):2: syntax error, unexpected ',', expecting end-of-input (SyntaxError)
...block: ->(e) { e << %i[a c] }},
...                              ^
	from /Users/me/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/method_source-1.0.0/lib/method_source/code_helpers.rb:71:in `block in complete_expression?'
	from /Users/me/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/method_source-1.0.0/lib/method_source/code_helpers.rb:70:in `catch'
	from /Users/me/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/method_source-1.0.0/lib/method_source/code_helpers.rb:70:in `complete_expression?'
	from /Users/me/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/method_source-1.0.0/lib/method_source/code_helpers.rb:97:in `block in extract_first_expression'
	from /Users/me/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/method_source-1.0.0/lib/method_source/code_helpers.rb:95:in `each'
	from /Users/me/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/method_source-1.0.0/lib/method_source/code_helpers.rb:95:in `extract_first_expression'
	from /Users/me/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/method_source-1.0.0/lib/method_source/code_helpers.rb:30:in `expression_at'
	from /Users/me/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/method_source-1.0.0/lib/method_source.rb:27:in `source_helper'
	from /Users/me/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/method_source-1.0.0/lib/method_source.rb:110:in `source'
	from ./source.rb:7:in `<main>'

Possible fix

I recognize this is a major change for the gem, and may not be possible due to the variety of ruby implementations this supports, but the parser gem does an excellent job turning any source file into an AST. The AST can easily be searched for proc/lambda/def calls, and the line numbers and source are easily accessible from any matching node.

This is the workaround I'm using for now to get Proc#source working for my case. I can adapt this and create a pull request for method_source if the maintainers feel that changing the parser is a good idea.

require "dry/core/cache"
require "parser/current"

module ProcSource
  extend Dry::Core::Cache
  extend AST::Sexp

  class SourceNotFound < StandardError; end

  # parse a ruby source file and return the AST; result is cached
  def self.parse(path)
    fetch_or_store(path) do
      source_buffer = Parser::Source::Buffer.new(path).read
      parser = Parser::CurrentRuby.new
      parser.diagnostics.all_errors_are_fatal = true
      parser.diagnostics.ignore_warnings      = true
      parser.parse(source_buffer)
    end
  end

  PROC_NODES = [
    s(:send, nil, :lambda),
    s(:send, nil, :proc),
    s(:send, s(:const, nil, :Proc), :new),
  ]

  module Helpers
    def source
      file, line = source_location
      root = ProcSource.parse(file)

      queue = [root]
      until queue.empty?
        node = queue.shift
        next unless node.is_a?(Parser::AST::Node)
        queue.unshift(*node.children)

        next unless node.type == :block
        next unless node.loc.line == line

        # verify the first child is a send node
        ch = node.children.first
        next unless ch.is_a?(Parser::AST::Node)
        next unless ch.type == :send

        # verify we're calling lambda, proc, or Proc.new
        next unless ProcSource::PROC_NODES.include?(ch)

        return node.loc.expression.source
      end

      raise SourceNotFound, "unable to find source for %p" % self
    end
  end

  Proc.prepend(Helpers)
end

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions