lib/rspec/expectations/block_snippet_extractor.rb
module RSpec module Expectations # @private class BlockSnippetExtractor # rubocop:disable Metrics/ClassLength # rubocop should properly handle `Struct.new {}` as an inner class definition. attr_reader :proc, :method_name def self.try_extracting_single_line_body_of(proc, method_name) lines = new(proc, method_name).body_content_lines return nil unless lines.count == 1 lines.first rescue Error nil end def initialize(proc, method_name) @proc = proc @method_name = method_name.to_s.freeze end # Ideally we should properly handle indentations of multiline snippet, # but it's not implemented yet since because we use result of this method only when it's a # single line and implementing the logic introduces additional complexity. def body_content_lines raw_body_lines.map(&:strip).reject(&:empty?) end private def raw_body_lines raw_body_snippet.split("\n") end def raw_body_snippet block_token_extractor.body_tokens.map(&:string).join end def block_token_extractor @block_token_extractor ||= BlockTokenExtractor.new(method_name, source, beginning_line_number) end if RSpec.respond_to?(:world) def source raise TargetNotFoundError unless File.exist?(file_path) RSpec.world.source_from_file(file_path) end else RSpec::Support.require_rspec_support 'source' def source raise TargetNotFoundError unless File.exist?(file_path) @source ||= RSpec::Support::Source.from_file(file_path) end end def file_path source_location.first end def beginning_line_number source_location.last end def source_location proc.source_location || raise(TargetNotFoundError) end Error = Class.new(StandardError) TargetNotFoundError = Class.new(Error) AmbiguousTargetError = Class.new(Error) # @private # Performs extraction of block body snippet using tokens, # which cannot be done with node information. BlockTokenExtractor = Struct.new(:method_name, :source, :beginning_line_number) do attr_reader :state, :body_tokens def initialize(*) super parse! end private def parse! @state = :initial catch(:finish) do source.tokens.each do |token| invoke_state_handler(token) end end end def finish! throw :finish end def invoke_state_handler(token) __send__("#{state}_state", token) end def initial_state(token) @state = :after_method_call if token.location == block_locator.method_call_location end def after_method_call_state(token) @state = :after_opener if handle_opener_token(token) end def after_opener_state(token) if handle_closer_token(token) finish_or_find_next_block_if_incorrect! elsif pipe_token?(token) finalize_pending_tokens! @state = :after_beginning_of_args else pending_tokens << token handle_opener_token(token) @state = :after_beginning_of_body unless token.type == :on_sp end end def after_beginning_of_args_state(token) @state = :after_beginning_of_body if pipe_token?(token) end def after_beginning_of_body_state(token) if handle_closer_token(token) finish_or_find_next_block_if_incorrect! else pending_tokens << token handle_opener_token(token) end end def pending_tokens @pending_tokens ||= [] end def finalize_pending_tokens! pending_tokens.freeze.tap do @pending_tokens = nil end end def finish_or_find_next_block_if_incorrect! body_tokens = finalize_pending_tokens! if correct_block?(body_tokens) @body_tokens = body_tokens finish! else @state = :after_method_call end end def handle_opener_token(token) opener_token?(token).tap do |boolean| opener_token_stack.push(token) if boolean end end def opener_token?(token) token.type == :on_lbrace || (token.type == :on_kw && token.string == 'do') end def handle_closer_token(token) if opener_token_stack.last.closed_by?(token) opener_token_stack.pop opener_token_stack.empty? else false end end def opener_token_stack @opener_token_stack ||= [] end def pipe_token?(token) token.type == :on_op && token.string == '|' end def correct_block?(body_tokens) return true if block_locator.body_content_locations.empty? content_location = block_locator.body_content_locations.first content_location.between?(body_tokens.first.location, body_tokens.last.location) end def block_locator @block_locator ||= BlockLocator.new(method_name, source, beginning_line_number) end end # @private # Locates target block with node information (semantics), which tokens don't have. BlockLocator = Struct.new(:method_name, :source, :beginning_line_number) do def method_call_location @method_call_location ||= method_ident_node.location end def body_content_locations @body_content_locations ||= block_body_node.map(&:location).compact end private def method_ident_node method_call_node = block_wrapper_node.children.first method_call_node.find do |node| method_ident_node?(node) end end def block_body_node block_node = block_wrapper_node.children[1] block_node.children.last end def block_wrapper_node case candidate_block_wrapper_nodes.size when 1 candidate_block_wrapper_nodes.first when 0 raise TargetNotFoundError else raise AmbiguousTargetError end end def candidate_block_wrapper_nodes @candidate_block_wrapper_nodes ||= candidate_method_ident_nodes.map do |method_ident_node| block_wrapper_node = method_ident_node.each_ancestor.find { |node| node.type == :method_add_block } next nil unless block_wrapper_node method_call_node = block_wrapper_node.children.first method_call_node.include?(method_ident_node) ? block_wrapper_node : nil end.compact end def candidate_method_ident_nodes source.nodes_by_line_number[beginning_line_number].select do |node| method_ident_node?(node) end end def method_ident_node?(node) node.type == :@ident && node.args.first == method_name end end end end end