Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion appmap.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ Gem::Specification.new do |spec|
spec.extensions << "ext/appmap/extconf.rb"
spec.require_paths = ['lib']

spec.add_dependency 'method_source'
spec.add_dependency 'reverse_markdown'

spec.add_runtime_dependency 'activesupport'
Expand Down
22 changes: 22 additions & 0 deletions lib/appmap/method_source/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
MIT License

Copyright (c) 2011 John Mair (banisterfiend)

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1 change: 1 addition & 0 deletions lib/appmap/method_source/NOTE
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This source code is copied from https://github.com/banister/method_source.
180 changes: 180 additions & 0 deletions lib/appmap/method_source/method_source.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# coding: utf-8
# (C) John Mair (banisterfiend) 2011
# MIT License

direc = File.dirname(__FILE__)

require "#{direc}/method_source/version"
require "#{direc}/method_source/source_location"
require "#{direc}/method_source/code_helpers"

module AppMap
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

# 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
# @param [String] method_name
# @return [String] The method body
def self.source_helper(source_location, name=nil)
raise SourceNotFoundError, "Could not locate source for #{name}!" unless source_location
file, line = *source_location

expression_at(lines_for(file), line)
rescue SyntaxError => e
raise SourceNotFoundError, "Could not parse source for #{name}: #{e.message}"
end

# Helper method responsible for opening source file and buffering up
# the comments for a specified method. Defined here to avoid polluting
# `Method` class.
# @param [Array] source_location The array returned by Method#source_location
# @param [String] method_name
# @return [String] The comments up to the point of the method.
def self.comment_helper(source_location, name=nil)
raise SourceNotFoundError, "Could not locate source for #{name}!" unless source_location
file, line = *source_location

comment_describing(lines_for(file), line)
end

# Load a memoized copy of the lines in a file.
#
# @param [String] file_name
# @param [String] method_name
# @return [Array<String>] the contents of the file
# @raise [SourceNotFoundError]
def self.lines_for(file_name, name=nil)
@lines_for_file ||= {}
@lines_for_file[file_name] ||= File.readlines(file_name)
rescue Errno::ENOENT => e
raise SourceNotFoundError, "Could not load source for #{name}: #{e.message}"
end

# Clear cache.
def self.clear_cache
@lines_for_file = {}
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
# provides the `#source` functionality
module MethodExtensions

# We use the included hook to patch Method#source on rubinius.
# We need to use the included hook as Rubinius defines a `source`
# on Method so including a module will have no effect (as it's
# higher up the MRO).
# @param [Class] klass The class that includes the module.
def self.included(klass)
if klass.method_defined?(:source) && Object.const_defined?(:RUBY_ENGINE) &&
RUBY_ENGINE =~ /rbx/

klass.class_eval do
orig_source = instance_method(:source)

define_method(:source) do
begin
super
rescue
orig_source.bind(self).call
end
end

end
end
end

# Return the sourcecode for the method as a string
# @return [String] The method sourcecode as a string
# @raise SourceNotFoundException
#
# @example
# Set.instance_method(:clear).source.display
# =>
# def clear
# @hash.clear
# self
# end
def source
AppMap::MethodSource.source_helper(source_location, defined?(name) ? name : inspect)
end

# Return the comments associated with the method as a string.
# @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
AppMap::MethodSource.comment_helper(source_location, defined?(name) ? name : inspect)
end

# Return the comments associated with the method class/module.
# @return [String] The method's comments as a string
# @raise SourceNotFoundException
#
# @example
# MethodSource::MethodExtensions.method(:included).module_comment
# =>
# # This module is to be included by `Method` and `UnboundMethod` and
# # provides the `#source` functionality
def class_comment
if self.respond_to?(:receiver)
class_inst_or_module = self.receiver
elsif self.respond_to?(:owner)
class_inst_or_module = self.owner
else
return comment
end

if class_inst_or_module.respond_to?(:name)
const_name = class_inst_or_module.name
else
const_name = class_inst_or_module.class.name
class_inst_or_module = class_inst_or_module.class
end

location = class_inst_or_module.const_source_location(const_name)

AppMap::MethodSource.comment_helper(location, defined?(name) ? name : inspect)
end
alias module_comment class_comment
end
end
end

class Method
include AppMap::MethodSource::SourceLocation::MethodExtensions
include AppMap::MethodSource::MethodExtensions
end

class UnboundMethod
include AppMap::MethodSource::SourceLocation::UnboundMethodExtensions
include AppMap::MethodSource::MethodExtensions
end

class Proc
include AppMap::MethodSource::SourceLocation::ProcExtensions
include AppMap::MethodSource::MethodExtensions
end

156 changes: 156 additions & 0 deletions lib/appmap/method_source/method_source/code_helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
module AppMap
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 [Integer] line_number The line number at which to look.
# NOTE: The first line in a file is
# line 1!
# @param [Hash] options The optional configuration parameters.
# @option options [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.
# @option options [Integer] :consume A number of lines to automatically
# consume (add to the expression buffer) without checking for validity.
# @return [String] The first complete expression
# @raise [SyntaxError] If the first complete expression can't be identified
def expression_at(file, line_number, options={})
options = {
:strict => false,
:consume => 0
}.merge!(options)

lines = file.is_a?(Array) ? file : file.each_line.to_a

relevant_lines = lines[(line_number - 1)..-1] || []

extract_first_expression(relevant_lines, options[:consume])
rescue SyntaxError => e
raise if options[:strict]

begin
extract_first_expression(relevant_lines) do |code|
code.gsub(/\#\{.*?\}/, "temp")
end
rescue SyntaxError
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 [Integer] 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 [Integer] consume A number of lines to automatically
# consume (add to the expression buffer) without checking for validity.
# @yield a clean-up function to run before checking for complete_expression
# @return [String] a valid ruby expression
# @raise [SyntaxError]
def extract_first_expression(lines, consume=0, &block)
code = consume.zero? ? "" : lines.slice!(0..(consume - 1)).join

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.reverse_each do |line|
# Add any line that is a valid ruby comment, and stop as
# soon as we hit a non comment line.
if (line =~ /^\s*#/) || (line =~ /^\s*$/)
buffer.append(line.lstrip)
else
break
end
end

buffer.reverse.join()
end

# An exception matcher that matches only subsets of SyntaxErrors that can be
# fixed by adding more input to the buffer.
module IncompleteExpression
GENERIC_REGEXPS = [
/unexpected (\$end|end-of-file|end-of-input|END_OF_FILE)/, # mri, jruby, ruby-2.0, ironruby
/embedded document meets end of file/, # =begin
/unterminated (quoted string|string|regexp|list) meets end of file/, # "quoted string" is ironruby
/can't find string ".*" anywhere before EOF/, # rbx and jruby
/missing 'end' for/, /expecting kWHEN/ # rbx
]

RBX_ONLY_REGEXPS = [
/expecting '[})\]]'(?:$|:)/, /expecting keyword_end/
]

def self.===(ex)
return false unless SyntaxError === ex
case ex.message
when *GENERIC_REGEXPS
true
when *RBX_ONLY_REGEXPS
rbx?
else
false
end
end

def self.rbx?
RbConfig::CONFIG['ruby_install_name'] == 'rbx'
end
end
end
end
end
Loading