diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/method_source.rb | 139 | ||||
-rw-r--r-- | lib/method_source/code_helpers.rb | 125 |
2 files changed, 168 insertions, 96 deletions
diff --git a/lib/method_source.rb b/lib/method_source.rb index 0353144..fa6bc3b 100644 --- a/lib/method_source.rb +++ b/lib/method_source.rb @@ -5,76 +5,27 @@ direc = File.dirname(__FILE__) require "#{direc}/method_source/version" require "#{direc}/method_source/source_location" +require "#{direc}/method_source/code_helpers" module MethodSource + extend MethodSource::CodeHelpers + # An Exception to mark errors that were raised trying to find the source from + # a given source_location. + # class SourceNotFoundError < StandardError; end - # Determine if a string of code is a valid Ruby expression. - # @param [String] code The code to validate. - # @return [Boolean] Whether or not the code is a valid Ruby expression. - # @example - # valid_expression?("class Hello") #=> false - # valid_expression?("class Hello; end") #=> true - def self.valid_expression?(str) - if defined?(Rubinius::Melbourne19) && RUBY_VERSION =~ /^1\.9/ - Rubinius::Melbourne19.parse_string(str) - elsif defined?(Rubinius::Melbourne) - Rubinius::Melbourne.parse_string(str) - else - catch(:valid) { - eval("BEGIN{throw :valid}\n#{str}") - } - end - true - rescue SyntaxError - false - end - # Helper method responsible for extracting method body. # Defined here to avoid polluting `Method` class. # @param [Array] source_location The array returned by Method#source_location # @return [String] The method body def self.source_helper(source_location) - return nil if !source_location.is_a?(Array) - - # 1st try: simple eval - code = extract_code(source_location) - - unless code - # 2nd try: attempt to re-scan method body, this time, assume we're inside an eval string simulate interpolation of #{} expressions by replacing it with placeholder - # - # A temporary work around for cases where method body is defined inside a - # string (i.e. class_evaled methods), and the resulting valid_expression - # doesn't return true due to string not being interpolated. - # (see https://github.com/banister/method_source/issues/13) - # - code = extract_code(source_location) { |code| code.gsub(/\#\{.*?\}/,"temp") } - end + raise SourceNotFoundError, "Source location not found" unless source_location + file, line = *source_location - code - rescue Errno::ENOENT - raise SourceNotFoundError, "Cannot get source code located at file: #{source_location[0]}" - end - - # @param [Array] source_location The array containing file_name [String], line [Fixnum] - # @param [Block] An optional block that can be passed that will be used to modify - # the code buffer before its syntax is evaluated - # @return [String] The method body - def self.extract_code(source_location) - file_name, line = source_location - code = "" - lines_for_file(file_name)[(line - 1)..-1].each do |line| - code << line - expression = block_given? ? yield(code) : code - return code if valid_expression?(expression) - end - nil - end - - def self.lines_for_file(file_name) - @lines_for_file ||= {} - @lines_for_file[file_name] ||= File.readlines(file_name) + expression_at(lines_for(file), line) + rescue SyntaxError => e + raise SourceNotFoundError, e.message end # Helper method responsible for opening source file and buffering up @@ -83,24 +34,34 @@ module MethodSource # @param [Array] source_location The array returned by Method#source_location # @return [String] The comments up to the point of the method. def self.comment_helper(source_location) - return nil if !source_location.is_a?(Array) - - file_name, line = source_location - File.open(file_name) do |file| - buffer = "" - (line - 1).times do - line = file.readline - # Add any line that is a valid ruby comment, - # but clear as soon as we hit a non comment line. - if (line =~ /^\s*#/) || (line =~ /^\s*$/) - buffer << line.lstrip - else - buffer.replace("") - end - end + raise SourceNotFoundError, "Source location not found" unless source_location + file, line = *source_location - buffer - end + comment_describing(lines_for(file), line) + end + + # Load a memoized copy of the lines in a file. + # + # @param [String] file_name + # @return [Array<String>] the contents of the file + # @raise [SourceNotFoundError] + def self.lines_for(file_name) + @lines_for_file ||= {} + @lines_for_file[file_name] ||= File.readlines(file_name) + rescue Errno::ENOENT => e + raise SourceNotFoundError, e.message + end + + # @deprecated — use MethodSource::CodeHelpers#complete_expression? + def self.valid_expression?(str) + complete_expression?(str) + rescue SyntaxError + false + end + + # @deprecated — use MethodSource::CodeHelpers#expression_at + def self.extract_code(source_location) + source_helper(source_location) end # This module is to be included by `Method` and `UnboundMethod` and @@ -132,8 +93,9 @@ module MethodSource end # Return the sourcecode for the method as a string - # (This functionality is only supported in Ruby 1.9 and above) # @return [String] The method sourcecode as a string + # @raise SourceNotFoundException + # # @example # Set.instance_method(:clear).source.display # => @@ -142,34 +104,19 @@ module MethodSource # self # end def source - if respond_to?(:source_location) - source = MethodSource.source_helper(source_location) - - raise SourceNotFoundError, "Cannot locate source for this method: #{name}" if !source - else - raise SourceNotFoundError, "#{self.class}#source not supported by this Ruby version (#{RUBY_VERSION})" - end - - source + MethodSource.source_helper(source_location) end # Return the comments associated with the method as a string. - # (This functionality is only supported in Ruby 1.9 and above) # @return [String] The method's comments as a string + # @raise SourceNotFoundException + # # @example # Set.instance_method(:clear).comment.display # => # # Removes all elements and returns self. def comment - if respond_to?(:source_location) - comment = MethodSource.comment_helper(source_location) - - raise SourceNotFoundError, "Cannot locate source for this method: #{name}" if !comment - else - raise SourceNotFoundError, "#{self.class}#comment not supported by this Ruby version (#{RUBY_VERSION})" - end - - comment + MethodSource.comment_helper(source_location) end end end diff --git a/lib/method_source/code_helpers.rb b/lib/method_source/code_helpers.rb new file mode 100644 index 0000000..173609e --- /dev/null +++ b/lib/method_source/code_helpers.rb @@ -0,0 +1,125 @@ +module MethodSource + + module CodeHelpers + # Retrieve the first expression starting on the given line of the given file. + # + # This is useful to get module or method source code. + # + # @param [Array<String>, File, String] file The file to parse, either as a File or as + # @param [Fixnum] line_number The line number at which to look. + # NOTE: The first line in a file is line 1! + # @param [Boolean] strict If set to true, then only completely valid expressions are + # returned. Otherwise heuristics are used to extract + # expressions that may have been valid inside an eval. + # @return [String] The first complete expression + # @raise [SyntaxError] If the first complete expression can't be identified + def expression_at(file, line_number, strict=false) + lines = file.is_a?(Array) ? file : file.each_line.to_a + + relevant_lines = lines[(line_number - 1)..-1] || [] + + extract_first_expression(relevant_lines) + rescue SyntaxError => e + raise if strict + + begin + extract_first_expression(relevant_lines) do |code| + code.gsub(/\#\{.*?\}/, "temp") + end + rescue SyntaxError => e2 + raise e + end + end + + # Retrieve the comment describing the expression on the given line of the given file. + # + # This is useful to get module or method documentation. + # + # @param [Array<String>, File, String] file The file to parse, either as a File or as + # a String or an Array of lines. + # @param [Fixnum] line_number The line number at which to look. + # NOTE: The first line in a file is line 1! + # @return [String] The comment + def comment_describing(file, line_number) + lines = file.is_a?(Array) ? file : file.each_line.to_a + + extract_last_comment(lines[0..(line_number - 2)]) + end + + # Determine if a string of code is a complete Ruby expression. + # @param [String] code The code to validate. + # @return [Boolean] Whether or not the code is a complete Ruby expression. + # @raise [SyntaxError] Any SyntaxError that does not represent incompleteness. + # @example + # complete_expression?("class Hello") #=> false + # complete_expression?("class Hello; end") #=> true + # complete_expression?("class 123") #=> SyntaxError: unexpected tINTEGER + def complete_expression?(str) + old_verbose = $VERBOSE + $VERBOSE = nil + + catch(:valid) do + eval("BEGIN{throw :valid}\n#{str}") + end + + # Assert that a line which ends with a , or \ is incomplete. + str !~ /[,\\]\s*\z/ + rescue IncompleteExpression + false + ensure + $VERBOSE = old_verbose + end + + private + + # Get the first expression from the input. + # + # @param [Array<String>] lines + # @param [&Block] a clean-up function to run before checking for complete_expression + # @return [String] a valid ruby expression + # @raise [SyntaxError] + def extract_first_expression(lines, &block) + code = "" + lines.each do |v| + code << v + return code if complete_expression?(block ? block.call(code) : code) + end + raise SyntaxError, "unexpected $end" + end + + # Get the last comment from the input. + # + # @param [Array<String>] lines + # @return [String] + def extract_last_comment(lines) + buffer = "" + + lines.each do |line| + # Add any line that is a valid ruby comment, + # but clear as soon as we hit a non comment line. + if (line =~ /^\s*#/) || (line =~ /^\s*$/) + buffer << line.lstrip + else + buffer.replace("") + end + end + + buffer + end + + # An exception matcher that matches only subsets of SyntaxErrors that can be + # fixed by adding more input to the buffer. + module IncompleteExpression + def self.===(ex) + case ex.message + when /unexpected (\$end|end-of-file|END_OF_FILE)/, # mri, jruby, ironruby + /unterminated (quoted string|string|regexp) meets end of file/, # "quoted string" is ironruby + /missing 'end' for/, /: expecting '[})\]]'$/, /can't find string ".*" anywhere before EOF/, /: expecting keyword_end/, /expecting kWHEN/ # rbx + true + else + false + end + end + end + end +end |