summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorConrad Irwin <conrad.irwin@gmail.com>2012-06-03 01:21:40 -0700
committerConrad Irwin <conrad.irwin@gmail.com>2012-06-03 01:51:19 -0700
commit38231ee9bd21624ab3859e1c953d634ceeb03930 (patch)
tree8d05121d850f7d545c438af92e481697cfba6387
parenta818f74dc498f4ed5dbb2d314fa7b5e1cdd49759 (diff)
downloadmethod_source-38231ee9bd21624ab3859e1c953d634ceeb03930.tar.gz
Import some CodeHelpers from Pry.
The hope is that they can live in this gem permanently, and Pry can also include MethodSource::CodeHelpers in the relevant places.
-rw-r--r--lib/method_source.rb139
-rw-r--r--lib/method_source/code_helpers.rb125
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