From 5075aa936878f95d7359662686defac6a6a0925a Mon Sep 17 00:00:00 2001 From: symwell <111290954+symwell@users.noreply.github.com> Date: Fri, 9 Sep 2022 14:02:01 -0400 Subject: [PATCH 1/4] add method_source source code without tests --- lib/appmap/method_source/method_source.rb | 177 ++++++++++++++++++ .../method_source/code_helpers.rb | 154 +++++++++++++++ .../method_source/source_location.rb | 138 ++++++++++++++ .../method_source/method_source/version.rb | 3 + 4 files changed, 472 insertions(+) create mode 100644 lib/appmap/method_source/method_source.rb create mode 100644 lib/appmap/method_source/method_source/code_helpers.rb create mode 100644 lib/appmap/method_source/method_source/source_location.rb create mode 100644 lib/appmap/method_source/method_source/version.rb diff --git a/lib/appmap/method_source/method_source.rb b/lib/appmap/method_source/method_source.rb new file mode 100644 index 00000000..ffd79ad3 --- /dev/null +++ b/lib/appmap/method_source/method_source.rb @@ -0,0 +1,177 @@ +# (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 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] 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 + 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 + 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) + + MethodSource.comment_helper(location, defined?(name) ? name : inspect) + end + alias module_comment class_comment + end +end + +class Method + include MethodSource::SourceLocation::MethodExtensions + include MethodSource::MethodExtensions +end + +class UnboundMethod + include MethodSource::SourceLocation::UnboundMethodExtensions + include MethodSource::MethodExtensions +end + +class Proc + include MethodSource::SourceLocation::ProcExtensions + include MethodSource::MethodExtensions +end + diff --git a/lib/appmap/method_source/method_source/code_helpers.rb b/lib/appmap/method_source/method_source/code_helpers.rb new file mode 100644 index 00000000..9ab2c732 --- /dev/null +++ b/lib/appmap/method_source/method_source/code_helpers.rb @@ -0,0 +1,154 @@ +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, 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, 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] 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] 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 diff --git a/lib/appmap/method_source/method_source/source_location.rb b/lib/appmap/method_source/method_source/source_location.rb new file mode 100644 index 00000000..76298697 --- /dev/null +++ b/lib/appmap/method_source/method_source/source_location.rb @@ -0,0 +1,138 @@ +module MethodSource + module ReeSourceLocation + # Ruby enterprise edition provides all the information that's + # needed, in a slightly different way. + def source_location + [__file__, __line__] rescue nil + end + end + + module SourceLocation + module MethodExtensions + if Proc.method_defined? :__file__ + include ReeSourceLocation + + elsif defined?(RUBY_ENGINE) && RUBY_ENGINE =~ /jruby/ + require 'java' + + # JRuby version source_location hack + # @return [Array] A two element array containing the source location of the method + def source_location + to_java.source_location(Thread.current.to_java.getContext()) + end + else + + + def trace_func(event, file, line, id, binding, classname) + return unless event == 'call' + set_trace_func nil + + @file, @line = file, line + raise :found + end + + private :trace_func + + # Return the source location of a method for Ruby 1.8. + # @return [Array] A two element array. First element is the + # file, second element is the line in the file where the + # method definition is found. + def source_location + if @file.nil? + args =[*(1..(arity<-1 ? -arity-1 : arity ))] + + set_trace_func method(:trace_func).to_proc + call(*args) rescue nil + set_trace_func nil + @file = File.expand_path(@file) if @file && File.exist?(File.expand_path(@file)) + end + [@file, @line] if @file + end + end + end + + module ProcExtensions + if Proc.method_defined? :__file__ + include ReeSourceLocation + + elsif defined?(RUBY_ENGINE) && RUBY_ENGINE =~ /rbx/ + + # Return the source location for a Proc (Rubinius only) + # @return [Array] A two element array. First element is the + # file, second element is the line in the file where the + # proc definition is found. + def source_location + [block.file.to_s, block.line] + end + else + + # Return the source location for a Proc (in implementations + # without Proc#source_location) + # @return [Array] A two element array. First element is the + # file, second element is the line in the file where the + # proc definition is found. + def source_location + self.to_s =~ /@(.*):(\d+)/ + [$1, $2.to_i] + end + end + end + + module UnboundMethodExtensions + if Proc.method_defined? :__file__ + include ReeSourceLocation + + elsif defined?(RUBY_ENGINE) && RUBY_ENGINE =~ /jruby/ + require 'java' + + # JRuby version source_location hack + # @return [Array] A two element array containing the source location of the method + def source_location + to_java.source_location(Thread.current.to_java.getContext()) + end + + else + + + # Return the source location of an instance method for Ruby 1.8. + # @return [Array] A two element array. First element is the + # file, second element is the line in the file where the + # method definition is found. + def source_location + klass = case owner + when Class + owner + when Module + method_owner = owner + Class.new { include(method_owner) } + end + + # deal with immediate values + case + when klass == Symbol + return :a.method(name).source_location + when klass == Integer + return 0.method(name).source_location + when klass == TrueClass + return true.method(name).source_location + when klass == FalseClass + return false.method(name).source_location + when klass == NilClass + return nil.method(name).source_location + end + + begin + Object.instance_method(:method).bind(klass.allocate).call(name).source_location + rescue TypeError + + # Assume we are dealing with a Singleton Class: + # 1. Get the instance object + # 2. Forward the source_location lookup to the instance + instance ||= ObjectSpace.each_object(owner).first + Object.instance_method(:method).bind(instance).call(name).source_location + end + end + end + end + end +end diff --git a/lib/appmap/method_source/method_source/version.rb b/lib/appmap/method_source/method_source/version.rb new file mode 100644 index 00000000..22f52de9 --- /dev/null +++ b/lib/appmap/method_source/method_source/version.rb @@ -0,0 +1,3 @@ +module MethodSource + VERSION = '1.0.0'.freeze +end From b58beb17c69657ac0b6ea787de1ae89f33ee54e7 Mon Sep 17 00:00:00 2001 From: symwell <111290954+symwell@users.noreply.github.com> Date: Fri, 9 Sep 2022 14:04:23 -0400 Subject: [PATCH 2/4] add method_source license --- lib/appmap/method_source/LICENSE | 22 ++++++++++++++++++++++ lib/appmap/method_source/NOTE | 1 + 2 files changed, 23 insertions(+) create mode 100644 lib/appmap/method_source/LICENSE create mode 100644 lib/appmap/method_source/NOTE diff --git a/lib/appmap/method_source/LICENSE b/lib/appmap/method_source/LICENSE new file mode 100644 index 00000000..a0976059 --- /dev/null +++ b/lib/appmap/method_source/LICENSE @@ -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. diff --git a/lib/appmap/method_source/NOTE b/lib/appmap/method_source/NOTE new file mode 100644 index 00000000..e9fb5a55 --- /dev/null +++ b/lib/appmap/method_source/NOTE @@ -0,0 +1 @@ +This source code is copied from https://github.com/banister/method_source. From 24c04bf3676357946a164509dbe94bcd06f57290 Mon Sep 17 00:00:00 2001 From: symwell <111290954+symwell@users.noreply.github.com> Date: Fri, 9 Sep 2022 14:13:25 -0400 Subject: [PATCH 3/4] rm dependency on method_source --- appmap.gemspec | 1 - 1 file changed, 1 deletion(-) diff --git a/appmap.gemspec b/appmap.gemspec index 4a96e425..170041fb 100644 --- a/appmap.gemspec +++ b/appmap.gemspec @@ -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' From 98f0458a2fb83de9bc1a005483b0c3432fca5c62 Mon Sep 17 00:00:00 2001 From: symwell <111290954+symwell@users.noreply.github.com> Date: Fri, 9 Sep 2022 14:39:50 -0400 Subject: [PATCH 4/4] Make MethodSource be AppMap::MethodSource --- lib/appmap/method_source/method_source.rb | 21 +++++++++++-------- .../method_source/code_helpers.rb | 2 ++ .../method_source/source_location.rb | 2 ++ .../method_source/method_source/version.rb | 2 ++ lib/appmap/trace.rb | 4 +++- 5 files changed, 21 insertions(+), 10 deletions(-) diff --git a/lib/appmap/method_source/method_source.rb b/lib/appmap/method_source/method_source.rb index ffd79ad3..72109eaa 100644 --- a/lib/appmap/method_source/method_source.rb +++ b/lib/appmap/method_source/method_source.rb @@ -1,3 +1,4 @@ +# coding: utf-8 # (C) John Mair (banisterfiend) 2011 # MIT License @@ -7,6 +8,7 @@ require "#{direc}/method_source/source_location" require "#{direc}/method_source/code_helpers" +module AppMap module MethodSource extend MethodSource::CodeHelpers @@ -112,7 +114,7 @@ def self.included(klass) # self # end def source - MethodSource.source_helper(source_location, defined?(name) ? name : inspect) + AppMap::MethodSource.source_helper(source_location, defined?(name) ? name : inspect) end # Return the comments associated with the method as a string. @@ -124,7 +126,7 @@ def source # => # # Removes all elements and returns self. def comment - MethodSource.comment_helper(source_location, defined?(name) ? name : inspect) + AppMap::MethodSource.comment_helper(source_location, defined?(name) ? name : inspect) end # Return the comments associated with the method class/module. @@ -154,24 +156,25 @@ def class_comment location = class_inst_or_module.const_source_location(const_name) - MethodSource.comment_helper(location, defined?(name) ? name : inspect) + AppMap::MethodSource.comment_helper(location, defined?(name) ? name : inspect) end alias module_comment class_comment end end +end class Method - include MethodSource::SourceLocation::MethodExtensions - include MethodSource::MethodExtensions + include AppMap::MethodSource::SourceLocation::MethodExtensions + include AppMap::MethodSource::MethodExtensions end class UnboundMethod - include MethodSource::SourceLocation::UnboundMethodExtensions - include MethodSource::MethodExtensions + include AppMap::MethodSource::SourceLocation::UnboundMethodExtensions + include AppMap::MethodSource::MethodExtensions end class Proc - include MethodSource::SourceLocation::ProcExtensions - include MethodSource::MethodExtensions + include AppMap::MethodSource::SourceLocation::ProcExtensions + include AppMap::MethodSource::MethodExtensions end diff --git a/lib/appmap/method_source/method_source/code_helpers.rb b/lib/appmap/method_source/method_source/code_helpers.rb index 9ab2c732..35eef627 100644 --- a/lib/appmap/method_source/method_source/code_helpers.rb +++ b/lib/appmap/method_source/method_source/code_helpers.rb @@ -1,3 +1,4 @@ +module AppMap module MethodSource module CodeHelpers @@ -152,3 +153,4 @@ def self.rbx? end end end +end diff --git a/lib/appmap/method_source/method_source/source_location.rb b/lib/appmap/method_source/method_source/source_location.rb index 76298697..ab6d5137 100644 --- a/lib/appmap/method_source/method_source/source_location.rb +++ b/lib/appmap/method_source/method_source/source_location.rb @@ -1,3 +1,4 @@ +module AppMap module MethodSource module ReeSourceLocation # Ruby enterprise edition provides all the information that's @@ -136,3 +137,4 @@ def source_location end end end +end diff --git a/lib/appmap/method_source/method_source/version.rb b/lib/appmap/method_source/method_source/version.rb index 22f52de9..b4907897 100644 --- a/lib/appmap/method_source/method_source/version.rb +++ b/lib/appmap/method_source/method_source/version.rb @@ -1,3 +1,5 @@ +module AppMap module MethodSource VERSION = '1.0.0'.freeze end +end diff --git a/lib/appmap/trace.rb b/lib/appmap/trace.rb index d5046777..3e5f345a 100644 --- a/lib/appmap/trace.rb +++ b/lib/appmap/trace.rb @@ -2,6 +2,8 @@ require 'delegate' +require 'appmap/method_source/method_source' + module AppMap module Trace class RubyMethod < SimpleDelegator @@ -22,7 +24,7 @@ def source_location def comment @method.comment - rescue MethodSource::SourceNotFoundError, Errno::EINVAL + rescue AppMap::MethodSource::SourceNotFoundError, Errno::EINVAL nil end