require 'ripper' require 'pp' class Proc TLAMBEG = [:on_tlambeg, "{"] TLAMBDA = [:on_tlambda, "->"] LBRACE = [:on_lbrace, '{'] TOKEN_PAIRS = {LBRACE => [:on_rbrace, '}'], [:on_kw, 'do'] => [:on_kw, 'end'], TLAMBDA => [:on_rbrace, '}']} # Make a best effort to provide the original source for a block # based on extracting a string from the file identified in # Proc#source_location using Ruby's tokenizer. # # This works for first block declared on a line in a source # file. If additional blocks are specified inside the first block # on the same line as the start of the block, only the outer-most # block declaration will be identified as a the block we want. # # If you require only the source of blocks-within-other-blocks, start them # on a new line as would be best practice for clarity and readability. def source @source ||= begin file, line_no = source_location raise "no file provided by source_location: #{self}" if file.nil? raise "no line number provided for source_location: #{self}" if line_no.nil? tokens = Ripper.lex File.read(file) tokens_on_line = tokens.select {|pos, lbl, str| pos[0].eql?(line_no) } starting_token = tokens_on_line.detect do |pos, lbl, str| TOKEN_PAIRS.keys.include?([lbl, str]) && _actually_starting_a_proc?(tokens, [pos, lbl, str]) end starting_token_type = [starting_token[1], starting_token[2]] ending_token_type = TOKEN_PAIRS[starting_token_type] source_str = "" remaining_tokens = tokens[tokens.index(starting_token)..-1] nesting = -1 starting_nesting_token_types = if [TLAMBDA, LBRACE].include?(starting_token_type) [TLAMBDA, LBRACE] else [starting_token_type] end while token = remaining_tokens.shift token = [token[1], token[2]] # strip position source_str << token[1] nesting += 1 if starting_nesting_token_types.include? token is_ending_token = token.eql?(ending_token_type) break if is_ending_token && nesting.eql?(0) nesting -= 1 if is_ending_token end source_str end end def _actually_starting_a_proc?(tokens, tok) return true if tokens.index(tok).eql?(0) look_back = tokens.slice(0..tokens.index(tok)-1) look_back.pop if look_back.last.try(:[], 1).eql? :on_sp if [:on_tlambeg, :on_tlambda].include?(tok[1]) true else ![:on_comma, :on_lparen, :on_label].include?(look_back.last.try(:[], 1)) end end # Examines the source of a proc to extract the body by # removing the outermost block delimiters and any surrounding. # whitespace. # # Raises exception if the block takes arguments. # def source_body raise "Cannot extract proc body on non-zero arity" unless arity.eql?(0) tokens = Ripper.lex source body_start_idx = 2 body_end_idx = -1 if tokens[0][1].eql?(:on_tlambda) body_start_idx = tokens.index(tokens.detect { |t| t[1].eql?(:on_tlambeg) }) + 1 end body_tokens = tokens[body_start_idx..-1] body_tokens.pop # ending token of proc # remove trailing whitespace whitespace = [:on_sp, :on_nl, :on_ignored_nl] body_tokens.pop while whitespace.include?(body_tokens[-1][1]) # remove leading whitespace body_tokens.shift while whitespace.include?(body_tokens[0][1]) # put them back together body_tokens.map {|token| token[2] }.join end def self.from_source(prc_src) raise ArgumentError unless prc_src.kind_of?(String) prc = eval(prc_src) prc.instance_variable_set(:@source, prc_src) prc end end