<commit_before>require 'proc_matcher/version'
require 'rspec/matchers'

class ProcDescriptor
  require 'sourcify'

  extend Forwardable

  def initialize(proc, sexp = proc.to_sexp)
    @proc = proc
    @sexp = sexp
  end

  def ==(other)
    return false if other.arity != arity || other.lambda? != lambda? || other.count != count
    return true  if other.sexp_body == sexp_body

    other.sexp == sexp_with_parameters_from(other)
  end

  def to_source
    lambda? ? proc.to_source.sub('proc ', '-> ') : proc.to_source
  end

  protected

  attr_reader :proc, :sexp

  def_delegators :proc, :arity, :lambda?
  def_delegators :sexp, :sexp_body, :count

  def parameters
    sexp[2].sexp_body.to_a
  end

  private

  def sexp_with_parameters_from(other_proc)
    new_sexp = sexp
    other_proc.parameters.each_with_index do |param, index|
      new_sexp = rename_parameter(parameters[index], param, new_sexp)
    end
    new_sexp
  end

  def rename_parameter(from, to, sexp)
    new_sexp = replace_parameter_definition(from, to, sexp)
    new_sexp = replace_parameter_references(from, to, new_sexp)
    new_sexp
  end

  def replace_parameter_references(from, to, sexp)
    sexp.gsub(s(:lvar, from), s(:lvar, to))
  end

  def replace_parameter_definition(from, to, sexp)
    new_sexp = sexp.clone
    index = parameters.find_index { |parameter| parameter == from }
    new_sexp[2][index + 1] = to
    new_sexp
  end
end

module RSpec
  module Matchers
    define(:equal_proc) do
      match { descriptor_for(actual) == descriptor_for(expected) }

      description { "equal #{description_of(expected)}" }

      def description_of(object)
        super(descriptor_for(object).to_source)
      end

      private

      def descriptor_for(proc)
        ProcDescriptor.new(proc)
      end
    end
  end
end

# TODO: split proc equals logic into its own gem
# TODO: handle procs where source extraction fails
# TODO: handle proc being passed as a block rather than a parameter
# TODO: handle bindings
<commit_msg>Minor refactoring<commit_after>require 'proc_matcher/version'
require 'rspec/matchers'

class ProcDescriptor
  require 'sourcify'

  extend Forwardable

  def initialize(proc)
    @proc = proc
  end

  def ==(other)
    return false if other.arity != arity || other.lambda? != lambda? || other.count != count
    return true  if other.sexp_body == sexp_body

    other.sexp == sexp_with_parameters_from(other)
  end

  def to_source
    lambda? ? proc.to_source.sub('proc ', '-> ') : proc.to_source
  end

  protected

  attr_reader :proc

  def_delegators :proc, :arity,     :lambda?
  def_delegators :sexp, :sexp_body, :count

  def sexp
    @sexp ||= proc.to_sexp
  end

  def parameters
    sexp[2].sexp_body.to_a
  end

  private

  def sexp_with_parameters_from(other_proc)
    new_sexp = sexp
    other_proc.parameters.each_with_index do |to, index|
      from = parameters[index]
      new_sexp = rename_parameter(from, to, new_sexp)
    end
    new_sexp
  end

  def rename_parameter(from, to, sexp)
    new_sexp = replace_parameter_definition(from, to, sexp)
    replace_parameter_references(from, to, new_sexp)
  end

  def replace_parameter_definition(from, to, sexp)
    new_sexp = sexp.clone
    index = parameters.find_index { |parameter| parameter == from }
    new_sexp[2][index + 1] = to
    new_sexp
  end

  def replace_parameter_references(from, to, sexp)
    sexp.gsub(s(:lvar, from), s(:lvar, to))
  end
end

module RSpec
  module Matchers
    define(:equal_proc) do
      match { descriptor_for(actual) == descriptor_for(expected) }

      description { "equal #{description_of(expected)}" }

      def description_of(object)
        super(descriptor_for(object).to_source)
      end

      private

      def descriptor_for(proc)
        ProcDescriptor.new(proc)
      end
    end
  end
end

# TODO: split proc equals logic into its own gem
# TODO: handle procs where source extraction fails
# TODO: handle proc being passed as a block rather than a parameter
# TODO: handle bindings

{'fraction_non_alphanumeric': 0.09149676777722526, 'fraction_numerical': 0.0014917951268025858, 'mean_word_length': 3.4403973509933774, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1261549', 'n_tokens_mistral': 1385, 'n_tokens_neox': 1335, 'n_words': 343}