├── VERSION ├── Gemfile ├── .travis.yml ├── test ├── fixtures │ ├── sample_module.rb │ └── sample_class.rb ├── helper.rb └── test_fast_method_source │ ├── test_fast_method_source_comment_for.rb │ └── test_fast_method_source_source_for.rb ├── ext └── fast_method_source │ ├── extconf.rb │ ├── fast_method_source.c │ └── node.h ├── .gitignore ├── lib ├── fast_method_source │ └── core_ext.rb └── fast_method_source.rb ├── benchmarks ├── comment_and_source_for.rb ├── fms_vs_ms_comment_for.rb ├── fms_vs_ms_source_for.rb └── bench_helper.rb ├── Rakefile ├── LICENCE.txt ├── CHANGELOG.md ├── fast_method_source.gemspec ├── docs └── API.md └── README.md /VERSION: -------------------------------------------------------------------------------- 1 | 0.4.0 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.1 4 | - 2.2 5 | - ruby-head 6 | -------------------------------------------------------------------------------- /test/fixtures/sample_module.rb: -------------------------------------------------------------------------------- 1 | module SampleModule 2 | # Sample method 3 | def sample_method 4 | :sample_method 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'fast_method_source' 3 | 4 | require_relative 'fixtures/sample_class' 5 | require_relative 'fixtures/sample_module' 6 | -------------------------------------------------------------------------------- /ext/fast_method_source/extconf.rb: -------------------------------------------------------------------------------- 1 | require 'mkmf' 2 | 3 | $CFLAGS << ' -std=c99 -Wno-declaration-after-statement' 4 | 5 | have_func('rb_sym2str', 'ruby.h') 6 | 7 | create_makefile('fast_method_source/fast_method_source') 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | lib/bundler/man 11 | pkg 12 | rdoc 13 | spec/reports 14 | test/tmp 15 | test/version_tmp 16 | tmp 17 | *.bundle 18 | *.so 19 | *.o 20 | *.a 21 | mkmf.log 22 | *~ 23 | .#* 24 | doc/ 25 | Makefile 26 | -------------------------------------------------------------------------------- /lib/fast_method_source/core_ext.rb: -------------------------------------------------------------------------------- 1 | class Proc 2 | unless self.method_defined?(:name) 3 | protected 4 | 5 | def name 6 | self.inspect 7 | end 8 | end 9 | end 10 | 11 | [Method, UnboundMethod, Proc].each do |klass| 12 | klass.include FastMethodSource::MethodExtensions 13 | 14 | klass.class_eval do 15 | def comment_and_source 16 | self.comment + self.source 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /benchmarks/comment_and_source_for.rb: -------------------------------------------------------------------------------- 1 | require_relative 'bench_helper' 2 | 3 | method_list = SystemNavigation.new.all_rb_methods.select do |method| 4 | begin 5 | FastMethodSource.comment_and_source_for(method) 6 | rescue FastMethodSource::SourceNotFoundError, IOError 7 | end 8 | end 9 | 10 | puts "Sample methods: #{method_list.count}" 11 | 12 | Benchmark.bm do |bm| 13 | bm.report('FastMethodSource#comment_and_source_for') do 14 | method_list.each do |method| 15 | FastMethodSource.comment_and_source_for(method) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /benchmarks/fms_vs_ms_comment_for.rb: -------------------------------------------------------------------------------- 1 | require_relative 'bench_helper' 2 | 3 | method_list = SystemNavigation.new.all_rb_methods.select do |method| 4 | begin 5 | FastMethodSource.comment_for(method) 6 | rescue FastMethodSource::SourceNotFoundError, IOError 7 | end 8 | end 9 | 10 | puts "Sample methods: #{method_list.count}" 11 | 12 | Benchmark.bmbm do |bm| 13 | bm.report('FastMethodSource#comment') do 14 | method_list.each do |method| 15 | FastMethodSource.comment_for(method) 16 | end 17 | end 18 | 19 | bm.report('MethodSource#comment') do 20 | method_list.each do |method| 21 | method.comment 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake/testtask' 2 | require 'rake/extensiontask' 3 | require 'rake/clean' 4 | 5 | Rake::ExtensionTask.new do |ext| 6 | ext.name = 'fast_method_source' 7 | ext.ext_dir = 'ext/fast_method_source' 8 | ext.lib_dir = 'lib/fast_method_source' 9 | end 10 | 11 | task :cleanup do 12 | system 'rm -f lib/fast_method_source/fast_method_source.so' 13 | end 14 | 15 | task :release do 16 | puts "So you want to release this thing, right?" 17 | puts "Here are your instructions:" 18 | puts " 0. Make sure this guide is up to date" 19 | puts " 1. Update the benchmark in README.md" 20 | puts " 2. Push to RubyGems!" 21 | end 22 | 23 | task :test => [:cleanup, :clobber, :compile] do 24 | Rake::TestTask.new do |t| 25 | t.test_files = Dir.glob('test/**/test_*.rb') 26 | end 27 | end 28 | 29 | task default: :test 30 | -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015 Kyrylo Silin 2 | 3 | This software is provided 'as-is', without any express or implied 4 | warranty. In no event will the authors be held liable for any damages 5 | arising from the use of this software. 6 | 7 | Permission is granted to anyone to use this software for any purpose, 8 | including commercial applications, and to alter it and redistribute it 9 | freely, subject to the following restrictions: 10 | 11 | 1. The origin of this software must not be misrepresented; you must not 12 | claim that you wrote the original software. If you use this software 13 | in a product, an acknowledgment in the product documentation would be 14 | appreciated but is not required. 15 | 16 | 2. Altered source versions must be plainly marked as such, and must not be 17 | misrepresented as being the original software. 18 | 19 | 3. This notice may not be removed or altered from any source distribution. 20 | -------------------------------------------------------------------------------- /benchmarks/fms_vs_ms_source_for.rb: -------------------------------------------------------------------------------- 1 | require_relative 'bench_helper' 2 | 3 | method_list = SystemNavigation.new.all_rb_methods.select do |method| 4 | # Make sure that both libraries can parse all given methods. Sometimes 5 | # method_source raises an error when Fast Method Soruce doesn't. 6 | begin 7 | FastMethodSource.source_for(method) 8 | rescue FastMethodSource::SourceNotFoundError, IOError 9 | end 10 | 11 | begin 12 | method.source 13 | rescue MethodSource::SourceNotFoundError, Errno::ENOTDIR, NoMethodError 14 | end 15 | end 16 | 17 | puts "Sample methods: #{method_list.count}" 18 | 19 | Benchmark.bmbm do |bm| 20 | bm.report('FastMethodSource#source') do 21 | method_list.each do |method| 22 | FastMethodSource.source_for(method) 23 | end 24 | end 25 | 26 | bm.report('MethodSource#source') do 27 | method_list.each do |method| 28 | method.source 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Fast Method Source changelog 2 | ============================ 3 | 4 | ### v0.4.0 (June 18, 2015) 5 | 6 | * _Significantly_ improve speed of both `#soruce` and `#comment`. The trade-off 7 | is that you must indent methods properly. 8 | * Decrease memeory consumption 9 | 10 | ### v0.3.1 (June 11, 2015) 11 | 12 | * Considerable speed improvements 13 | 14 | ### v0.3.0 (June 11, 2015) 15 | 16 | * _Significantly_ reduced memory consumption (`#comment_and_source` for 19K 17 | methods uses about 100-150 MB of RES RAM) 18 | * _Significantly_ decreased time of calculations 19 | 20 | ### v0.2.0 (June 11, 2015) 21 | 22 | * Significantly reduced memory consumption 23 | * The time of calculations has increased by 6% 24 | * Fixed memory not being freed on exceptions 25 | * Fixed exit code value for shells [→](https://github.com/kyrylo/fast_method_source/pull/2) 26 | 27 | ### v0.1.1 (June 9, 2015) 28 | 29 | * Fixed segfault on any C method query 30 | 31 | ### v0.1.0 (June 9, 2015) 32 | 33 | * Initial release 34 | -------------------------------------------------------------------------------- /lib/fast_method_source.rb: -------------------------------------------------------------------------------- 1 | require_relative 'fast_method_source/fast_method_source' 2 | 3 | module FastMethodSource 4 | # The VERSION file must be in the root directory of the library. 5 | VERSION_FILE = File.expand_path('../../VERSION', __FILE__) 6 | 7 | VERSION = File.exist?(VERSION_FILE) ? 8 | File.read(VERSION_FILE).chomp : '(could not find VERSION file)' 9 | 10 | # The root path of Pry Theme source code. 11 | ROOT = File.expand_path(File.dirname(__FILE__)) 12 | 13 | class Method 14 | include MethodExtensions 15 | 16 | def initialize(method) 17 | @method = method 18 | end 19 | 20 | def source_location 21 | @method.source_location 22 | end 23 | 24 | def name 25 | if @method.respond_to?(:name) 26 | @method.name 27 | else 28 | @method.inspect 29 | end 30 | end 31 | end 32 | 33 | def self.source_for(method) 34 | FastMethodSource::Method.new(method).source 35 | end 36 | 37 | def self.comment_for(method) 38 | FastMethodSource::Method.new(method).comment 39 | end 40 | 41 | def self.comment_and_source_for(method) 42 | self.comment_for(method) + self.source_for(method) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /fast_method_source.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'fast_method_source' 3 | s.version = File.read('VERSION') 4 | s.date = Time.now.strftime('%Y-%m-%d') 5 | s.summary = 'Retrieves the source code for a method (and its comment)' 6 | s.description = 'Retrieves the source code for a method (and its comment) and does it faster than any other existing solution' 7 | s.author = 'Kyrylo Silin' 8 | s.email = 'silin@kyrylo.org' 9 | s.homepage = 'https://github.com/kyrylo/fast_method_source' 10 | s.licenses = 'Zlib' 11 | 12 | s.require_path = 'lib' 13 | s.files = %w[ 14 | ext/fast_method_source/extconf.rb 15 | ext/fast_method_source/fast_method_source.c 16 | ext/fast_method_source/node.h 17 | lib/fast_method_source.rb 18 | lib/fast_method_source/core_ext.rb 19 | VERSION 20 | README.md 21 | CHANGELOG.md 22 | LICENCE.txt 23 | ] 24 | s.extensions = ['ext/fast_method_source/extconf.rb'] 25 | s.platform = Gem::Platform::RUBY 26 | 27 | s.add_development_dependency 'bundler', '~> 1.0' 28 | s.add_development_dependency 'rake', '~> 10.4' 29 | s.add_development_dependency 'rake-compiler', '~> 0.9' 30 | s.add_development_dependency 'minitest', '~> 5.7' 31 | end 32 | -------------------------------------------------------------------------------- /benchmarks/bench_helper.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | require 'benchmark' 3 | require 'rbconfig' 4 | 5 | def require_dep(dep_name) 6 | require dep_name 7 | rescue LoadError => e 8 | name = e.message.scan(/(?<=-- ).+/).first 9 | raise e unless name 10 | name.gsub!('/', '-') 11 | puts "'#{name}' is not installed. To fix: gem install #{name}" 12 | exit 13 | end 14 | 15 | module Kernel 16 | def suppress_warnings 17 | original_verbosity = $VERBOSE 18 | $VERBOSE = nil 19 | result = yield 20 | $VERBOSE = original_verbosity 21 | return result 22 | end 23 | end 24 | 25 | require_dep 'require_all' 26 | require_dep 'method_source' 27 | require_dep 'system_navigation' 28 | require_dep 'sys/cpu' 29 | 30 | stdlib_files = File.join(RbConfig::CONFIG['rubylibdir'], "**/*.rb") 31 | IGNORE_LIST = [ 32 | /tkmacpkg.rb/, 33 | /macpkg.rb/, 34 | /debug.rb/, 35 | /multi-tk.rb/, 36 | /tk.rb/, 37 | /tkwinpkg.rb/, 38 | /winpkg.rb/, 39 | /ICONS.rb/i, 40 | /tkextlib/, 41 | /profile.rb/, 42 | /formatter_test_case.rb/, 43 | /test_case.rb/, 44 | /xmlparser/, 45 | /xmlscanner/, 46 | /cgi_runner.rb/, 47 | /multi-irb.rb/, 48 | /subirb.rb/, 49 | /ws-for-case-2.rb/, 50 | /test_utilities.rb/, 51 | /psych\/parser.rb/, 52 | /rake\/contrib\/sys.rb/, 53 | /rake\/gempackagetask.rb/, 54 | /rake\/rdoctask.rb/, 55 | /ruby182_test_unit_fix.rb/, 56 | /rake\/runtest.rb/, 57 | /tcl/, 58 | /minitest\/spec.rb/, 59 | /rubygems\/exceptions.rb/, 60 | /\/irb\//, 61 | /\/rake\//, 62 | /package\/old.rb/, 63 | /rubygems\/rdoc.rb/ 64 | ] 65 | 66 | files_to_require = Dir.glob(stdlib_files).reject do |p| 67 | IGNORE_LIST.any? { |pattern| pattern =~ p } 68 | end 69 | 70 | if $DEBUG 71 | print "Ignoring these files: " 72 | p Dir.glob(stdlib_files) - files_to_require 73 | 74 | puts "Requiring the rest of stdlib..." 75 | 76 | require_all files_to_require 77 | 78 | puts "Requiring fast_method_source from source..." 79 | require_relative '../lib/fast_method_source' 80 | else 81 | suppress_warnings { require_all files_to_require } 82 | end 83 | 84 | puts "Processor: #{Sys::CPU.processors.first.model_name}" 85 | puts "Platform: #{RUBY_ENGINE} #{RUBY_VERSION}p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE} revision #{RUBY_REVISION}) [#{RUBY_PLATFORM}]" 86 | puts "Counting the number of sample methods..." 87 | -------------------------------------------------------------------------------- /docs/API.md: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | Overview 5 | -- 6 | 7 | The library provides the following methods: 8 | 9 | * `#comment` 10 | * `#source` 11 | * `#comment_and_source` 12 | 13 | There are two ways to use Fast Method Source. One way is to monkey-patch 14 | relevant core Ruby classes and use the methods directly. 15 | 16 | ```ruby 17 | require 'fast_method_source' 18 | require 'fast_method_source/core_ext' # <= monkey-patches 19 | 20 | # Monkey-patch of UnboundMethod 21 | Set.instance_method(:merge).source #=> " def merge(enum)\n..." 22 | 23 | # Monkey-patch of Method 24 | Set.method(:[]).source #=> " def self.[](*ary)\n..." 25 | 26 | # Monkey-patch of Proc (or lambda) 27 | myproc = proc { |arg| 28 | arg + 1 29 | } 30 | myproc.source #=> "myproc = proc { |arg|\n..." 31 | ``` 32 | 33 | The other way is by using these methods defined on the library's class directly. 34 | 35 | ```ruby 36 | require 'fast_method_source' 37 | 38 | # With UnboundMethod 39 | FastMethodSource.source_for(Set.instance_method(:merge)) #=> " def merge(enum)\n..." 40 | 41 | # With Method 42 | FastMethodSource.source_for(Set.method(:[])) #=> " def self.[](*ary)\n..." 43 | 44 | # With Proc (or lambda) 45 | myproc = proc { |arg| 46 | arg + 1 47 | } 48 | FastMethodSource.source_for(myproc) #=> "myproc = proc { |arg|\n..." 49 | ``` 50 | 51 | Method Information 52 | -- 53 | 54 | #### FastMethodSource#source_for(method) 55 | 56 | Returns the source code of the given _method_ as a String. 57 | Raises `FastMethodSource::SourceNotFoundError` if: 58 | 59 | * _method_ is defined outside of a file (for example, in a REPL) 60 | * _method_ doesn't have a source location (typically C methods) 61 | 62 | Raises `IOError` if: 63 | 64 | * the file location of _method_ is inaccessible 65 | 66 | ```ruby 67 | FastMethodSource.source_for(Set.instance_method(:merge)) 68 | #=> " def merge(enum)\n..." 69 | FastMethodSource.source_for(Array.instance_method(:pop)) 70 | #=> FastMethodSource::SourceNotFoundError 71 | ``` 72 | 73 | #### FastMethodSource#comment_for(method) 74 | 75 | Returns the comment of the given _method_ as a String. The rest is identical to 76 | `FastMethodSource#source_for(method)`. 77 | 78 | #### FastMethodSource#comment_and_source_for(method) 79 | 80 | Returns the comment and the source code of the given _method_ as a String (the 81 | order is the same as the method's name). The rest is identical to 82 | `FastMethodSource#source_for(method)`. 83 | -------------------------------------------------------------------------------- /test/test_fast_method_source/test_fast_method_source_comment_for.rb: -------------------------------------------------------------------------------- 1 | require_relative '../helper' 2 | 3 | class TestFastMethodSource < Minitest::Test 4 | def test_comment_for_class 5 | method = SampleClass.instance_method(:sample_method) 6 | assert_match(/# Sample method/, FastMethodSource.comment_for(method)) 7 | end 8 | 9 | def test_comment_for_module 10 | method = SampleModule.instance_method(:sample_method) 11 | assert_match(/# Sample method/, FastMethodSource.comment_for(method)) 12 | end 13 | 14 | def test_comment_for_proc 15 | # Heavy rain 16 | method = proc { |*args| 17 | args.first + args.last 18 | } 19 | 20 | assert_match(/# Heavy rain/, FastMethodSource.comment_for(method)) 21 | end 22 | 23 | def test_comment_for_proc_instance 24 | # Heavy rain 25 | method = Proc.new { |*args| 26 | args.first + args.last 27 | } 28 | 29 | assert_match(/# Heavy rain/, FastMethodSource.comment_for(method)) 30 | end 31 | 32 | def test_comment_for_lambda 33 | # Life is strange 34 | method = lambda { |*args| 35 | args.first + args.last 36 | } 37 | 38 | assert_match(/# Life is strange/, FastMethodSource.comment_for(method)) 39 | end 40 | 41 | def test_comment_for_lambda_arrow 42 | # Life is strange 43 | method = -> *args { 44 | args.first + args.last 45 | } 46 | 47 | assert_match(/# Life is strange/, FastMethodSource.comment_for(method)) 48 | end 49 | 50 | def test_comment_for_set_merge 51 | require 'set' 52 | 53 | method = Set.instance_method(:merge) 54 | expected = %r{ 55 | \s* 56 | # Merges the elements of the given enumerable object to the set and\n 57 | \s* 58 | # returns self.\n 59 | }x 60 | assert_match(expected, FastMethodSource.comment_for(method)) 61 | end 62 | 63 | def test_comment_for_padding 64 | # Life is strange 65 | 66 | 67 | method = -> *args { 68 | args.first + args.last 69 | } 70 | 71 | assert_equal "", FastMethodSource.comment_for(method) 72 | end 73 | 74 | def test_comment_for_no_comment 75 | method = proc { 76 | args.first + args.last 77 | } 78 | 79 | assert_equal '', FastMethodSource.comment_for(method) 80 | end 81 | 82 | def test_comment_for_kernel_require 83 | method = SampleClass.instance_method(:kernel_require) 84 | assert_match(/##\n # When RubyGems is required, Kernel#require is replaced/, 85 | FastMethodSource.comment_for(method)) 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /test/fixtures/sample_class.rb: -------------------------------------------------------------------------------- 1 | class SampleClass 2 | attr_accessor :accessor 3 | 4 | attr_reader :reader1, 5 | :reader2 6 | 7 | # Sample method 8 | def sample_method 9 | :sample_method 10 | end 11 | 12 | def one_line_method; 1; end 13 | 14 | ## 15 | # When RubyGems is required, Kernel#require is replaced with our own which 16 | # is capable of loading gems on demand. 17 | # 18 | # When you call require 'x', this is what happens: 19 | # * If the file can be loaded from the existing Ruby loadpath, it 20 | # is. 21 | # * Otherwise, installed gems are searched for a file that matches. 22 | # If it's found in gem 'y', that gem is activated (added to the 23 | # loadpath). 24 | # 25 | # The normal require functionality of returning false if 26 | # that file has already been loaded is preserved. 27 | 28 | def kernel_require path 29 | RUBYGEMS_ACTIVATION_MONITOR.enter 30 | 31 | path = path.to_path if path.respond_to? :to_path 32 | 33 | spec = Gem.find_unresolved_default_spec(path) 34 | if spec 35 | Gem.remove_unresolved_default_spec(spec) 36 | gem(spec.name) 37 | end 38 | 39 | # If there are no unresolved deps, then we can use just try 40 | # normal require handle loading a gem from the rescue below. 41 | 42 | if Gem::Specification.unresolved_deps.empty? then 43 | RUBYGEMS_ACTIVATION_MONITOR.exit 44 | return gem_original_require(path) 45 | end 46 | 47 | # If +path+ is for a gem that has already been loaded, don't 48 | # bother trying to find it in an unresolved gem, just go straight 49 | # to normal require. 50 | #-- 51 | # TODO request access to the C implementation of this to speed up RubyGems 52 | 53 | spec = Gem::Specification.stubs.find { |s| 54 | s.activated? and s.contains_requirable_file? path 55 | } 56 | 57 | begin 58 | RUBYGEMS_ACTIVATION_MONITOR.exit 59 | return gem_original_require(spec.to_fullpath(path) || path) 60 | end if spec 61 | 62 | # Attempt to find +path+ in any unresolved gems... 63 | 64 | found_specs = Gem::Specification.find_in_unresolved path 65 | 66 | # If there are no directly unresolved gems, then try and find +path+ 67 | # in any gems that are available via the currently unresolved gems. 68 | # For example, given: 69 | # 70 | # a => b => c => d 71 | # 72 | # If a and b are currently active with c being unresolved and d.rb is 73 | # requested, then find_in_unresolved_tree will find d.rb in d because 74 | # it's a dependency of c. 75 | # 76 | if found_specs.empty? then 77 | found_specs = Gem::Specification.find_in_unresolved_tree path 78 | 79 | found_specs.each do |found_spec| 80 | found_spec.activate 81 | end 82 | 83 | # We found +path+ directly in an unresolved gem. Now we figure out, of 84 | # the possible found specs, which one we should activate. 85 | else 86 | 87 | # Check that all the found specs are just different 88 | # versions of the same gem 89 | names = found_specs.map(&:name).uniq 90 | 91 | if names.size > 1 then 92 | RUBYGEMS_ACTIVATION_MONITOR.exit 93 | raise Gem::LoadError, "#{path} found in multiple gems: #{names.join ', '}" 94 | end 95 | 96 | # Ok, now find a gem that has no conflicts, starting 97 | # at the highest version. 98 | valid = found_specs.select { |s| s.conflicts.empty? }.last 99 | 100 | unless valid then 101 | le = Gem::LoadError.new "unable to find a version of '#{names.first}' to activate" 102 | le.name = names.first 103 | RUBYGEMS_ACTIVATION_MONITOR.exit 104 | raise le 105 | end 106 | 107 | valid.activate 108 | end 109 | 110 | RUBYGEMS_ACTIVATION_MONITOR.exit 111 | return gem_original_require(path) 112 | rescue LoadError => load_error 113 | RUBYGEMS_ACTIVATION_MONITOR.enter 114 | 115 | if load_error.message.start_with?("Could not find") or 116 | (load_error.message.end_with?(path) and Gem.try_activate(path)) then 117 | RUBYGEMS_ACTIVATION_MONITOR.exit 118 | return gem_original_require(path) 119 | else 120 | RUBYGEMS_ACTIVATION_MONITOR.exit 121 | end 122 | 123 | raise load_error 124 | end 125 | 126 | def rdoc_escape string 127 | string.gsub(/["\\\t\n]/) do |special_character| 128 | case special_character 129 | when "\t" 130 | "\\t" 131 | when "\n" 132 | "\\n" 133 | else 134 | "\\#{special_character}" 135 | end 136 | end 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fast Method Source 2 | 3 | [![Build Status](https://travis-ci.org/kyrylo/fast_method_source.svg?branch=master)](https://travis-ci.org/kyrylo/fast_method_source) 4 | 5 | * Repository: [https://github.com/kyrylo/fast_method_source/][repo] 6 | * API description: [https://github.com/kyrylo/fast_method_source/blob/master/docs/API.md][apimd] 7 | 8 | Description 9 | ----------- 10 | 11 | Fast Method Source is a Ruby library for querying methods, procs and lambdas for 12 | their source code and comments. 13 | 14 | ```ruby 15 | require 'fast_method_source' 16 | require 'fast_method_source/core_ext' # Adds #source to UnboundMethod 17 | require 'set' 18 | 19 | puts Set.instance_method(:merge).source 20 | ``` 21 | 22 | Output. 23 | 24 | ``` 25 | def merge(enum) 26 | if enum.instance_of?(self.class) 27 | @hash.update(enum.instance_variable_get(:@hash)) 28 | else 29 | do_with_enum(enum) { |o| add(o) } 30 | end 31 | 32 | self 33 | end 34 | ``` 35 | 36 | Fast Method Source provides functionality similar to [method_source][ms]. 37 | However, it is significantly faster. It can show the source for 19K methods in 38 | roughly a second, while method_source takes about 30 seconds. The benchmarks 39 | below show the difference expressed in numbers. 40 | 41 | #### #source 42 | ##### ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-linux] 43 | 44 | The speed boost of method_source after the rehearsal that can be observed in 45 | this benchmark comes from the fact that it uses memoization. Fast Method Source 46 | does not support it at the moment. 47 | 48 | ``` 49 | Processor: Intel(R) Core(TM) i5-2410M CPU @ 2.30GHz 50 | Platform: ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-linux] 51 | Counting the number of sample methods... 52 | Sample methods: 19280 53 | Rehearsal ----------------------------------------------------------- 54 | FastMethodSource#source 0.760000 0.240000 1.000000 ( 1.104455) 55 | MethodSource#source 53.240000 0.250000 53.490000 ( 59.304424) 56 | ------------------------------------------------- total: 54.490000sec 57 | 58 | user system total real 59 | FastMethodSource#source 0.610000 0.190000 0.800000 ( 0.890971) 60 | MethodSource#source 51.770000 0.230000 52.000000 ( 57.640986) 61 | ``` 62 | 63 | #### #comment 64 | ##### ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-linux] 65 | 66 | ``` 67 | Processor: Intel(R) Core(TM) i5-2410M CPU @ 2.30GHz 68 | Platform: ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-linux] 69 | Counting the number of sample methods... 70 | Sample methods: 19437 71 | Rehearsal ------------------------------------------------------------ 72 | FastMethodSource#comment 0.180000 0.170000 0.350000 ( 0.395569) 73 | MethodSource#comment 19.860000 0.230000 20.090000 ( 36.373289) 74 | -------------------------------------------------- total: 20.440000sec 75 | 76 | user system total real 77 | FastMethodSource#comment 0.270000 0.190000 0.460000 ( 0.520414) 78 | MethodSource#comment 19.360000 0.020000 19.380000 ( 21.726528) 79 | ``` 80 | 81 | #### #comment_and_source 82 | ##### ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-linux] 83 | 84 | This is a convenience method, and method_source doesn't have it. 85 | 86 | ``` 87 | Processor: Intel(R) Core(TM) i5-2410M CPU @ 2.30GHz 88 | Platform: ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-linux] 89 | Counting the number of sample methods... 90 | Sample methods: 19435 91 | user system total real 92 | FastMethodSource#comment_and_source_for 0.810000 0.420000 1.230000 ( 1.374435) 93 | ``` 94 | 95 | API 96 | --- 97 | 98 | Read here: [https://github.com/kyrylo/fast_method_source/blob/master/docs/API.md][apimd] 99 | 100 | Installation 101 | ------------ 102 | 103 | All you need is to install the gem. 104 | 105 | gem install fast_method_source 106 | 107 | 108 | Limitations 109 | ----------- 110 | 111 | ### Rubies 112 | 113 | * CRuby 2.1.x 114 | * CRuby 2.2.x 115 | * CRuby 2.3.0dev and higher 116 | 117 | Rubies below 2.2.2 were not tested, so in theory if it quacks like Ruby 2, it 118 | may work. 119 | 120 | ### Operation Systems 121 | 122 | * GNU/Linux 123 | * Mac OS (hopefully) 124 | 125 | Licence 126 | ------- 127 | 128 | The project uses Zlib License. See the LICENCE.txt file for more information. 129 | 130 | [repo]: https://github.com/kyrylo/fast_method_source/ "Home page" 131 | [ms]: https://github.com/banister/method_source 132 | [apimd]: https://github.com/kyrylo/fast_method_source/blob/master/docs/API.md 133 | -------------------------------------------------------------------------------- /test/test_fast_method_source/test_fast_method_source_source_for.rb: -------------------------------------------------------------------------------- 1 | require_relative '../helper' 2 | 3 | class TestFastMethodSource < Minitest::Test 4 | def test_source_for_class 5 | method = SampleClass.instance_method(:sample_method) 6 | expected = " def sample_method\n :sample_method\n end\n" 7 | assert_equal expected, FastMethodSource.source_for(method) 8 | end 9 | 10 | def test_source_for_module 11 | method = SampleModule.instance_method(:sample_method) 12 | expected = " def sample_method\n :sample_method\n end\n" 13 | assert_equal expected, FastMethodSource.source_for(method) 14 | end 15 | 16 | def test_source_for_proc 17 | method = proc { |*args| 18 | args.first + args.last 19 | } 20 | 21 | expected = " method = proc { |*args|\n args.first + args.last\n }\n" 22 | assert_equal expected, FastMethodSource.source_for(method) 23 | end 24 | 25 | def test_source_for_proc_instance 26 | method = Proc.new { |*args| 27 | args.first + args.last 28 | } 29 | 30 | expected = " method = Proc.new { |*args|\n args.first + args.last\n }\n" 31 | assert_equal expected, FastMethodSource.source_for(method) 32 | end 33 | 34 | def test_source_for_lambda 35 | method = lambda { |*args| 36 | args.first + args.last 37 | } 38 | 39 | expected = " method = lambda { |*args|\n args.first + args.last\n }\n" 40 | assert_equal expected, FastMethodSource.source_for(method) 41 | end 42 | 43 | def test_source_for_lambda_arrow 44 | method = -> *args { 45 | args.first + args.last 46 | } 47 | 48 | expected = " method = -> *args {\n args.first + args.last\n }\n" 49 | assert_equal expected, FastMethodSource.source_for(method) 50 | end 51 | 52 | def test_source_for_rss_image_favicon 53 | require 'rss' 54 | 55 | method = RSS::ImageFaviconModel::ImageFavicon.instance_method(:dc_date_elements) 56 | expected = %r{klass.install_have_children_element\(name, DC_URI, \".+\n\s+full_name, full_plural_name\)} 57 | assert_match expected, FastMethodSource.source_for(method) 58 | end 59 | 60 | def test_source_for_erb_scan 61 | require 'erb' 62 | 63 | method = ERB::Compiler::SimpleScanner2.instance_method(:scan) 64 | expected = /def scan\n.+yield\(scanner.+end\n\s+end\n\z/m 65 | assert_match(expected, FastMethodSource.source_for(method)) 66 | end 67 | 68 | def test_source_for_irb_change_workspace 69 | require 'irb' 70 | 71 | method = IRB::ExtendCommandBundle.instance_method(:irb_change_workspace) 72 | expected = /def \#{cmd_name}\(\*opts, &b\).+\n\s+end\n\z/m 73 | assert_match(expected, FastMethodSource.source_for(method)) 74 | end 75 | 76 | def test_source_for_rss_time_w3cdtf 77 | require 'rss' 78 | 79 | method = Time.instance_method(:w3cdtf) 80 | expected = /def w3cdtf\n if usec.zero\?\n.+end\n\z/m 81 | assert_match(expected, FastMethodSource.source_for(method)) 82 | end 83 | 84 | def test_source_for_kernel_require 85 | method = SampleClass.instance_method(:kernel_require) 86 | expected = /def kernel_require path\n RUBYGEMS_ACTIVATION_MONITOR.enter\n.+raise load_error\n\s+end\n\z/m 87 | assert_match(expected, FastMethodSource.source_for(method)) 88 | end 89 | 90 | def test_source_for_irb_init_config 91 | require 'irb' 92 | 93 | method = IRB.singleton_class.instance_method(:init_config) 94 | expected = /def IRB.init_config\(ap_path\)\n # class instance variables\n.+@CONF\[:DEBUG_LEVEL\] = 0\n\s+end\n\z/m 95 | assert_match(expected, FastMethodSource.source_for(method)) 96 | end 97 | 98 | def test_source_for_rss_def_classed_element 99 | require 'rss' 100 | 101 | method = RSS::Maker::Base.singleton_class.instance_method(:def_classed_element) 102 | expected = /def def_classed_element\(name, class_name=nil, attribute_name=nil\)\n.+attr_reader name\n\s+end\n\s+end\n\z/m 103 | assert_match(expected, FastMethodSource.source_for(method)) 104 | end 105 | 106 | def test_source_for_rss_square_brackets 107 | require 'rss' 108 | 109 | method = REXML::Elements.instance_method(:[]) 110 | expected = /def \[\]\( index, name=nil\)\n.+return nil\n.+end\n\z/m 111 | assert_match(expected, FastMethodSource.source_for(method)) 112 | end 113 | 114 | def test_source_for_core_ext 115 | require 'fast_method_source/core_ext' 116 | 117 | method = proc { |*args| 118 | args.first + args.last 119 | } 120 | 121 | assert_raises(NameError) { Pry } 122 | assert_raises(NameError) { MethodSource } 123 | 124 | expected = " method = proc { |*args|\n args.first + args.last\n }\n" 125 | assert_equal expected, method.source 126 | end 127 | 128 | def test_source_for_mkmf_configuration 129 | require 'mkmf' 130 | 131 | method = MakeMakefile.instance_method(:configuration) 132 | expected = /def configuration\(srcdir\)\n.+mk\n\s+end\n\z/m 133 | assert_match(expected, FastMethodSource.source_for(method)) 134 | end 135 | 136 | def test_source_for_rdoc_setup_parser 137 | require 'rdoc' 138 | 139 | method = RDoc::Markdown.instance_method(:setup_parser) 140 | expected = /def setup_parser\(str, debug=false\)\n.+setup_foreign_grammar\n\s+end\n\z/m 141 | assert_match(expected, FastMethodSource.source_for(method)) 142 | end 143 | 144 | def test_source_for_rdoc_escape 145 | method = SampleClass.instance_method(:rdoc_escape) 146 | expected = /def rdoc_escape string\n.+end\n end\n end\n\z/m 147 | assert_match(expected, FastMethodSource.source_for(method)) 148 | end 149 | 150 | def test_source_for_rss_to_feed 151 | require 'rss' 152 | 153 | method = RSS::Maker::DublinCoreModel::DublinCoreDatesBase.instance_method(:to_feed) 154 | expected = /def to_feed\(\*args\)\n.+end\n\s+end\n\z/m 155 | assert_match(expected, FastMethodSource.source_for(method)) 156 | end 157 | 158 | def test_source_for_rdoc_find_attr_comment 159 | require 'rdoc' 160 | 161 | method = RDoc::Parser::C.instance_method(:find_attr_comment) 162 | expected = /def find_attr_comment var_name, attr_name, read.+RDoc::Comment.new comment, @top_level\n end\n\z/m 163 | assert_match(expected, FastMethodSource.source_for(method)) 164 | end 165 | 166 | def test_source_for_fileutils_cd 167 | require 'fileutils' 168 | 169 | method = FileUtils::LowMethods.instance_method(:cd) 170 | 171 | # Reports wrong source_location (lineno 1981 instead of 1980). 172 | # https://github.com/ruby/ruby/blob/a9721a259665149b1b9ff0beabcf5f8dc0136120/lib/fileutils.rb#L1681 173 | expected = /:commands, :options, :have_option\?, :options_of, :collect_method\]\n\n/ 174 | assert_match expected, FastMethodSource.source_for(method) 175 | end 176 | 177 | def test_source_for_process_utime 178 | method = Process::Tms.instance_method(:utime=) 179 | 180 | # Process::Tms.instance_method(:utime=).source_location 181 | # #=> ["
", nil] 182 | assert_raises(FastMethodSource::SourceNotFoundError) { 183 | FastMethodSource.source_for(method) 184 | } 185 | end 186 | 187 | def test_source_for_etc_name 188 | method = Process::Tms.instance_method(:utime=) 189 | 190 | # Etc::Group.instance_method(:name=).source_location 191 | # #=> ["/opt/rubies/druby-2.2.2/lib/ruby/2.2.0/x86_64-linux/etc.so", nil] 192 | assert_raises(FastMethodSource::SourceNotFoundError) { 193 | FastMethodSource.source_for(method) 194 | } 195 | end 196 | 197 | def test_source_for_c_method 198 | method = Array.instance_method(:pop) 199 | 200 | assert_raises(FastMethodSource::SourceNotFoundError) { 201 | FastMethodSource.source_for(method) 202 | } 203 | end 204 | 205 | def test_source_for_accessor 206 | method = SampleClass.instance_method(:accessor) 207 | assert_match(/attr_accessor :accessor\n\z/, FastMethodSource.source_for(method)) 208 | end 209 | 210 | def test_source_for_multiline_reader 211 | method = SampleClass.instance_method(:reader2) 212 | assert_match(/attr_reader :reader1,\n\s*:reader2\n\z/, 213 | FastMethodSource.source_for(method)) 214 | end 215 | 216 | def test_source_for_rss_author 217 | require 'rss' 218 | 219 | # source_location returns bad line number (complete nonsense) and 220 | # method_source raises an exception when tries to parse this. In case of 221 | # Fast Method Source, it returns the closest method. Let's just make sure 222 | # here that it returns at least something. 223 | method = RSS::Atom::Feed::Entry.instance_method(:author=) 224 | assert_equal false, FastMethodSource.source_for(method).empty? 225 | end 226 | 227 | def test_source_for_one_line_method 228 | method = SampleClass.instance_method(:one_line_method) 229 | expected = " def one_line_method; 1; end\n" 230 | assert_equal(expected, FastMethodSource.source_for(method)) 231 | end 232 | 233 | def test_source_for_rss_content 234 | require 'rss' 235 | 236 | method = RSS::DublinCoreModel::DublinCoreDate.instance_method(:content=) 237 | expected = /class DublinCore.+\n.+:content=\)\s+end\n\z/m 238 | assert_match expected, FastMethodSource.source_for(method) 239 | end 240 | end 241 | -------------------------------------------------------------------------------- /ext/fast_method_source/fast_method_source.c: -------------------------------------------------------------------------------- 1 | // For getline(), fileno() and friends 2 | #define _XOPEN_SOURCE 700 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "node.h" 12 | 13 | #ifdef _WIN32 14 | #include 15 | static const char *null_filename = "NUL"; 16 | #define DUP(fd) _dup(fd) 17 | #define DUP2(fd, newfd) _dup2(fd, newfd) 18 | #else 19 | #include 20 | static const char *null_filename = "/dev/null"; 21 | #define DUP(fd) dup(fd) 22 | #define DUP2(fd, newfd) dup2(fd, newfd) 23 | #endif 24 | 25 | #ifndef HAVE_RB_SYM2STR 26 | # define rb_sym2str(obj) rb_id2str(SYM2ID(obj)) 27 | #endif 28 | 29 | #ifdef __APPLE__ 30 | void * 31 | memrchr(const void *s, int c, size_t n) 32 | { 33 | const unsigned char *cp; 34 | 35 | if (n != 0) { 36 | cp = (unsigned char *)s + n; 37 | do { 38 | if (*(--cp) == (unsigned char)c) 39 | return((void *)cp); 40 | } while (--n != 0); 41 | } 42 | return NULL; 43 | } 44 | #else 45 | void *memrchr(const void *s, int c, size_t n); 46 | #endif 47 | 48 | typedef struct { 49 | int source : 1; 50 | int comment : 1; 51 | } finder; 52 | 53 | struct method_data { 54 | unsigned method_location; 55 | const char *filename; 56 | VALUE method_name; 57 | }; 58 | 59 | static VALUE read_lines(finder finder, struct method_data *data); 60 | static VALUE read_lines_before(struct method_data *data); 61 | static VALUE read_lines_after(struct method_data *data); 62 | static VALUE find_method_comment(struct method_data *data); 63 | static VALUE find_method_source(struct method_data *data); 64 | static VALUE find_comment_expression(struct method_data *data); 65 | static VALUE find_source_expression(struct method_data *data); 66 | static NODE *parse_expr(VALUE rb_str); 67 | static NODE *parse_with_silenced_stderr(VALUE rb_str); 68 | static void restore_ch(char *line, char ch, int last_ch_idx); 69 | static void close_map(int fd, char *map, size_t map_size, const char *filename); 70 | static void nulify_ch(char *line, char *ch, int last_ch_idx); 71 | static int contains_end_kw(const char *line); 72 | static int is_comment(const char *line, const size_t line_len); 73 | static int is_definition_end(const char *line); 74 | static int is_static_definition_start(const char *line); 75 | static size_t count_prefix_spaces(const char *line); 76 | static void raise_if_nil(VALUE val, VALUE method_name); 77 | static void method_data_init(VALUE self, struct method_data *data); 78 | static VALUE mMethodExtensions_source(VALUE self); 79 | 80 | static VALUE rb_eSourceNotFoundError; 81 | 82 | static VALUE 83 | find_method_source(struct method_data *data) 84 | { 85 | return read_lines_after(data); 86 | } 87 | 88 | static VALUE 89 | find_method_comment(struct method_data *data) 90 | { 91 | return read_lines_before(data); 92 | } 93 | 94 | static VALUE 95 | read_lines_after(struct method_data *data) 96 | { 97 | finder finder = {1, 0}; 98 | return read_lines(finder, data); 99 | } 100 | 101 | static VALUE 102 | read_lines_before(struct method_data *data) 103 | { 104 | finder finder = {0, 1}; 105 | return read_lines(finder, data); 106 | } 107 | 108 | static VALUE 109 | find_comment_expression(struct method_data *data) 110 | { 111 | struct stat filestat; 112 | int fd; 113 | char *map; 114 | 115 | VALUE comment = Qnil; 116 | 117 | if ((fd = open(data->filename, O_RDONLY)) == -1) { 118 | rb_raise(rb_eIOError, "failed to read - %s", data->filename); 119 | } 120 | 121 | if (fstat(fd, &filestat) == -1) { 122 | rb_raise(rb_eIOError, "filestat failed for %s", data->filename); 123 | } 124 | 125 | map = mmap(NULL, filestat.st_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0); 126 | if (map == MAP_FAILED) { 127 | rb_raise(rb_eIOError, "mmap failed for %s", data->filename); 128 | } 129 | 130 | char *rest = map; 131 | unsigned line_count = 0; 132 | 133 | while (line_count++ != data->method_location - 1) { 134 | rest = (char *) memchr(rest, '\n', filestat.st_size) + 1; 135 | } 136 | 137 | size_t end = filestat.st_size - strlen(rest); 138 | map[end] = '\0'; 139 | 140 | size_t offset = end; 141 | char *prev = NULL; 142 | 143 | while (line_count-- != 0) { 144 | rest = memrchr(map, '\n', offset - 2); 145 | 146 | if (rest == NULL) { 147 | rest = memrchr(map, '\n', offset); 148 | } else { 149 | rest++; 150 | } 151 | 152 | offset = end - strlen(rest); 153 | 154 | if (is_comment(rest, offset)) { 155 | prev = rest; 156 | } else { 157 | if (prev == NULL) { 158 | comment = rb_str_new("", 0); 159 | } else { 160 | comment = rb_str_new2(prev); 161 | } 162 | 163 | break; 164 | } 165 | } 166 | 167 | if (munmap (map, filestat.st_size) == -1) { 168 | rb_raise(rb_eIOError, "munmap failed for %s", data->filename); 169 | } 170 | close(fd); 171 | 172 | return comment; 173 | } 174 | 175 | static VALUE 176 | find_source_expression(struct method_data *data) 177 | { 178 | struct stat filestat; 179 | int fd; 180 | char *map; 181 | 182 | VALUE source = Qnil; 183 | 184 | if ((fd = open(data->filename, O_RDONLY)) == -1) { 185 | rb_raise(rb_eIOError, "failed to read - %s", data->filename); 186 | } 187 | 188 | if (fstat(fd, &filestat) == -1) { 189 | rb_raise(rb_eIOError, "filestat failed for %s", data->filename); 190 | } 191 | 192 | map = mmap(NULL, filestat.st_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0); 193 | if (map == MAP_FAILED) { 194 | rb_raise(rb_eIOError, "mmap failed for %s", data->filename); 195 | } 196 | 197 | char *expr_start = map; 198 | char *expr_end, *next_expr, *line; 199 | unsigned line_count = 0; 200 | 201 | while (line_count++ != data->method_location - 1) { 202 | expr_start = (char *) memchr(expr_start, '\n', filestat.st_size) + 1; 203 | } 204 | 205 | size_t last_ch_idx; 206 | char ch, next_ch; 207 | next_expr = expr_end = expr_start; 208 | size_t leftover_size = filestat.st_size - strlen(expr_end); 209 | size_t offset = filestat.st_size - leftover_size; 210 | size_t next_offset; 211 | size_t prefix_len = 0; 212 | int inside_static_def = 0; 213 | int should_parse = 0; 214 | VALUE rb_expr; 215 | 216 | while (offset != 0) { 217 | next_expr = (char *) memchr(expr_end, '\n', offset) + 1; 218 | next_offset = strlen(next_expr); 219 | line = expr_end; 220 | 221 | last_ch_idx = offset - next_offset - 1; 222 | 223 | nulify_ch(line, &ch, last_ch_idx); 224 | 225 | expr_end = next_expr; 226 | offset = next_offset; 227 | 228 | if (is_static_definition_start(line) && !inside_static_def) { 229 | inside_static_def = 1; 230 | prefix_len = count_prefix_spaces(line); 231 | 232 | if (contains_end_kw(line)) { 233 | if (parse_expr(rb_str_new2(line)) != NULL) { 234 | restore_ch(line, ch, last_ch_idx); 235 | rb_expr = rb_str_new2(expr_start); 236 | 237 | close_map(fd, map, filestat.st_size, data->filename); 238 | return rb_expr; 239 | } 240 | } 241 | } 242 | 243 | if (line[0] == '\n' || is_comment(line, strlen(line))) { 244 | line[last_ch_idx] = ch; 245 | continue; 246 | } 247 | 248 | if (inside_static_def) { 249 | if (is_definition_end(line) && 250 | count_prefix_spaces(line) == prefix_len) 251 | { 252 | restore_ch(line, ch, last_ch_idx); 253 | rb_expr = rb_str_new2(expr_start); 254 | 255 | close_map(fd, map, filestat.st_size, data->filename); 256 | return rb_expr; 257 | } else { 258 | line[last_ch_idx] = ch; 259 | continue; 260 | } 261 | } else { 262 | should_parse = 1; 263 | } 264 | 265 | if (should_parse) { 266 | line[last_ch_idx] = ch; 267 | nulify_ch(line, &next_ch, last_ch_idx + 1); 268 | 269 | if (parse_expr(rb_str_new2(expr_start)) != NULL) { 270 | rb_expr = rb_str_new2(expr_start); 271 | close_map(fd, map, filestat.st_size, data->filename); 272 | return rb_expr; 273 | } else { 274 | line[last_ch_idx+1] = next_ch; 275 | } 276 | } 277 | 278 | line[last_ch_idx] = ch; 279 | } 280 | close_map(fd, map, filestat.st_size, data->filename); 281 | 282 | return source; 283 | } 284 | 285 | static VALUE 286 | read_lines(finder finder, struct method_data *data) 287 | { 288 | if (finder.comment) { 289 | return find_comment_expression(data); 290 | } else if (finder.source) { 291 | return find_source_expression(data); 292 | } 293 | 294 | return Qnil; 295 | } 296 | 297 | static NODE * 298 | parse_with_silenced_stderr(VALUE rb_str) 299 | { 300 | int old_stderr; 301 | FILE *null_fd; 302 | VALUE last_exception = rb_errinfo(); 303 | 304 | old_stderr = DUP(STDERR_FILENO); 305 | fflush(stderr); 306 | null_fd = fopen(null_filename, "w"); 307 | DUP2(fileno(null_fd), STDERR_FILENO); 308 | 309 | volatile VALUE vparser = rb_parser_new(); 310 | NODE *node = rb_parser_compile_string(vparser, "-", rb_str, 1); 311 | rb_set_errinfo(last_exception); 312 | 313 | fflush(stderr); 314 | fclose(null_fd); 315 | 316 | DUP2(old_stderr, STDERR_FILENO); 317 | close(old_stderr); 318 | 319 | return node; 320 | } 321 | 322 | static NODE * 323 | parse_expr(VALUE rb_str) { 324 | return parse_with_silenced_stderr(rb_str); 325 | } 326 | 327 | static void 328 | restore_ch(char *line, char ch, int last_ch_idx) 329 | { 330 | line[last_ch_idx] = ch; 331 | line[last_ch_idx + 1] = '\0'; 332 | } 333 | 334 | static void 335 | close_map(int fd, char *map, size_t map_size, const char *filename) 336 | { 337 | close(fd); 338 | if (munmap(map, map_size) == -1) { 339 | rb_raise(rb_eIOError, "munmap failed for %s", filename); 340 | } 341 | } 342 | 343 | static void 344 | nulify_ch(char *line, char *ch, int last_ch_idx) 345 | { 346 | *ch = line[last_ch_idx]; 347 | line[last_ch_idx] = '\0'; 348 | } 349 | 350 | static size_t 351 | count_prefix_spaces(const char *line) 352 | { 353 | int i = 0; 354 | size_t spaces = 0; 355 | 356 | do { 357 | if (line[i] == ' ') { 358 | spaces++; 359 | } else { 360 | break; 361 | } 362 | } while (line[i++] != '\n'); 363 | 364 | return spaces; 365 | } 366 | 367 | static int 368 | is_definition_end(const char *line) 369 | { 370 | int i = 0; 371 | 372 | do { 373 | if (line[i] == ' ') { 374 | continue; 375 | } else if (strncmp((line + i), "end", 3) == 0) { 376 | return 1; 377 | } else { 378 | return 0; 379 | } 380 | } while (line[i++] != '\0'); 381 | 382 | return 0; 383 | } 384 | 385 | static int 386 | contains_end_kw(const char *line) 387 | { 388 | char *match; 389 | char prev_ch; 390 | 391 | if ((match = strstr(line, "end")) != NULL) { 392 | prev_ch = (match - 1)[0]; 393 | return prev_ch == ' ' || prev_ch == '\0' || prev_ch == ';'; 394 | } else { 395 | return 0; 396 | } 397 | } 398 | 399 | static int 400 | is_comment(const char *line, const size_t line_len) 401 | { 402 | for (size_t i = 0; i < line_len; i++) { 403 | if (line[i] == ' ') 404 | continue; 405 | 406 | return line[i] == '#' && line[i + 1] != '{'; 407 | } 408 | 409 | return 0; 410 | } 411 | 412 | static int 413 | is_static_definition_start(const char *line) 414 | { 415 | int i = 0; 416 | 417 | do { 418 | if (line[i] == ' ') { 419 | continue; 420 | } else if (strncmp((line + i), "def ", 4) == 0) { 421 | return 1; 422 | } else if (strncmp((line + i), "class ", 6) == 0) { 423 | return 1; 424 | } else { 425 | return 0; 426 | } 427 | } while (line[i++] != '\0'); 428 | 429 | return 0; 430 | } 431 | 432 | static void 433 | raise_if_nil(VALUE val, VALUE method_name) 434 | { 435 | if (NIL_P(val)) { 436 | if (SYMBOL_P(method_name)) { 437 | rb_raise(rb_eSourceNotFoundError, "could not locate source for %s", 438 | RSTRING_PTR(rb_sym2str(method_name))); 439 | } else { 440 | rb_raise(rb_eSourceNotFoundError, "could not locate source for %s", 441 | RSTRING_PTR(method_name)); 442 | } 443 | } 444 | } 445 | 446 | static void 447 | method_data_init(VALUE self, struct method_data *data) 448 | { 449 | VALUE source_location = rb_funcall(self, rb_intern("source_location"), 0); 450 | VALUE name = rb_funcall(self, rb_intern("name"), 0); 451 | 452 | raise_if_nil(source_location, name); 453 | 454 | VALUE rb_filename = RARRAY_AREF(source_location, 0); 455 | VALUE rb_method_location = RARRAY_AREF(source_location, 1); 456 | 457 | raise_if_nil(rb_filename, name); 458 | raise_if_nil(rb_method_location, name); 459 | 460 | data->filename = RSTRING_PTR(rb_filename); 461 | data->method_location = FIX2INT(rb_method_location); 462 | data->method_name = name; 463 | } 464 | 465 | static VALUE 466 | mMethodExtensions_source(VALUE self) 467 | { 468 | struct method_data data; 469 | method_data_init(self, &data); 470 | 471 | VALUE source = find_method_source(&data); 472 | raise_if_nil(source, data.method_name); 473 | 474 | return source; 475 | } 476 | 477 | static VALUE 478 | mMethodExtensions_comment(VALUE self) 479 | { 480 | struct method_data data; 481 | method_data_init(self, &data); 482 | 483 | VALUE comment = find_method_comment(&data); 484 | raise_if_nil(comment, data.method_name); 485 | 486 | return comment; 487 | } 488 | 489 | void Init_fast_method_source(void) 490 | { 491 | VALUE rb_mFastMethodSource = rb_define_module_under(rb_cObject, "FastMethodSource"); 492 | 493 | rb_eSourceNotFoundError = rb_define_class_under(rb_mFastMethodSource,"SourceNotFoundError", rb_eStandardError); 494 | VALUE rb_mMethodExtensions = rb_define_module_under(rb_mFastMethodSource, "MethodExtensions"); 495 | 496 | rb_define_method(rb_mMethodExtensions, "source", mMethodExtensions_source, 0); 497 | rb_define_method(rb_mMethodExtensions, "comment", mMethodExtensions_comment, 0); 498 | } 499 | -------------------------------------------------------------------------------- /ext/fast_method_source/node.h: -------------------------------------------------------------------------------- 1 | /********************************************************************** 2 | 3 | node.h - 4 | 5 | $Author$ 6 | created at: Fri May 28 15:14:02 JST 1993 7 | 8 | Copyright (C) 1993-2007 Yukihiro Matsumoto 9 | 10 | **********************************************************************/ 11 | 12 | #ifndef RUBY_NODE_H 13 | #define RUBY_NODE_H 1 14 | 15 | #if defined(__cplusplus) 16 | extern "C" { 17 | #if 0 18 | } /* satisfy cc-mode */ 19 | #endif 20 | #endif 21 | 22 | enum node_type { 23 | NODE_SCOPE, 24 | #define NODE_SCOPE NODE_SCOPE 25 | NODE_BLOCK, 26 | #define NODE_BLOCK NODE_BLOCK 27 | NODE_IF, 28 | #define NODE_IF NODE_IF 29 | NODE_CASE, 30 | #define NODE_CASE NODE_CASE 31 | NODE_WHEN, 32 | #define NODE_WHEN NODE_WHEN 33 | NODE_OPT_N, 34 | #define NODE_OPT_N NODE_OPT_N 35 | NODE_WHILE, 36 | #define NODE_WHILE NODE_WHILE 37 | NODE_UNTIL, 38 | #define NODE_UNTIL NODE_UNTIL 39 | NODE_ITER, 40 | #define NODE_ITER NODE_ITER 41 | NODE_FOR, 42 | #define NODE_FOR NODE_FOR 43 | NODE_BREAK, 44 | #define NODE_BREAK NODE_BREAK 45 | NODE_NEXT, 46 | #define NODE_NEXT NODE_NEXT 47 | NODE_REDO, 48 | #define NODE_REDO NODE_REDO 49 | NODE_RETRY, 50 | #define NODE_RETRY NODE_RETRY 51 | NODE_BEGIN, 52 | #define NODE_BEGIN NODE_BEGIN 53 | NODE_RESCUE, 54 | #define NODE_RESCUE NODE_RESCUE 55 | NODE_RESBODY, 56 | #define NODE_RESBODY NODE_RESBODY 57 | NODE_ENSURE, 58 | #define NODE_ENSURE NODE_ENSURE 59 | NODE_AND, 60 | #define NODE_AND NODE_AND 61 | NODE_OR, 62 | #define NODE_OR NODE_OR 63 | NODE_MASGN, 64 | #define NODE_MASGN NODE_MASGN 65 | NODE_LASGN, 66 | #define NODE_LASGN NODE_LASGN 67 | NODE_DASGN, 68 | #define NODE_DASGN NODE_DASGN 69 | NODE_DASGN_CURR, 70 | #define NODE_DASGN_CURR NODE_DASGN_CURR 71 | NODE_GASGN, 72 | #define NODE_GASGN NODE_GASGN 73 | NODE_IASGN, 74 | #define NODE_IASGN NODE_IASGN 75 | NODE_IASGN2, 76 | #define NODE_IASGN2 NODE_IASGN2 77 | NODE_CDECL, 78 | #define NODE_CDECL NODE_CDECL 79 | NODE_CVASGN, 80 | #define NODE_CVASGN NODE_CVASGN 81 | NODE_CVDECL, 82 | #define NODE_CVDECL NODE_CVDECL 83 | NODE_OP_ASGN1, 84 | #define NODE_OP_ASGN1 NODE_OP_ASGN1 85 | NODE_OP_ASGN2, 86 | #define NODE_OP_ASGN2 NODE_OP_ASGN2 87 | NODE_OP_ASGN_AND, 88 | #define NODE_OP_ASGN_AND NODE_OP_ASGN_AND 89 | NODE_OP_ASGN_OR, 90 | #define NODE_OP_ASGN_OR NODE_OP_ASGN_OR 91 | NODE_OP_CDECL, 92 | #define NODE_OP_CDECL NODE_OP_CDECL 93 | NODE_CALL, 94 | #define NODE_CALL NODE_CALL 95 | NODE_FCALL, 96 | #define NODE_FCALL NODE_FCALL 97 | NODE_VCALL, 98 | #define NODE_VCALL NODE_VCALL 99 | NODE_SUPER, 100 | #define NODE_SUPER NODE_SUPER 101 | NODE_ZSUPER, 102 | #define NODE_ZSUPER NODE_ZSUPER 103 | NODE_ARRAY, 104 | #define NODE_ARRAY NODE_ARRAY 105 | NODE_ZARRAY, 106 | #define NODE_ZARRAY NODE_ZARRAY 107 | NODE_VALUES, 108 | #define NODE_VALUES NODE_VALUES 109 | NODE_HASH, 110 | #define NODE_HASH NODE_HASH 111 | NODE_RETURN, 112 | #define NODE_RETURN NODE_RETURN 113 | NODE_YIELD, 114 | #define NODE_YIELD NODE_YIELD 115 | NODE_LVAR, 116 | #define NODE_LVAR NODE_LVAR 117 | NODE_DVAR, 118 | #define NODE_DVAR NODE_DVAR 119 | NODE_GVAR, 120 | #define NODE_GVAR NODE_GVAR 121 | NODE_IVAR, 122 | #define NODE_IVAR NODE_IVAR 123 | NODE_CONST, 124 | #define NODE_CONST NODE_CONST 125 | NODE_CVAR, 126 | #define NODE_CVAR NODE_CVAR 127 | NODE_NTH_REF, 128 | #define NODE_NTH_REF NODE_NTH_REF 129 | NODE_BACK_REF, 130 | #define NODE_BACK_REF NODE_BACK_REF 131 | NODE_MATCH, 132 | #define NODE_MATCH NODE_MATCH 133 | NODE_MATCH2, 134 | #define NODE_MATCH2 NODE_MATCH2 135 | NODE_MATCH3, 136 | #define NODE_MATCH3 NODE_MATCH3 137 | NODE_LIT, 138 | #define NODE_LIT NODE_LIT 139 | NODE_STR, 140 | #define NODE_STR NODE_STR 141 | NODE_DSTR, 142 | #define NODE_DSTR NODE_DSTR 143 | NODE_XSTR, 144 | #define NODE_XSTR NODE_XSTR 145 | NODE_DXSTR, 146 | #define NODE_DXSTR NODE_DXSTR 147 | NODE_EVSTR, 148 | #define NODE_EVSTR NODE_EVSTR 149 | NODE_DREGX, 150 | #define NODE_DREGX NODE_DREGX 151 | NODE_DREGX_ONCE, 152 | #define NODE_DREGX_ONCE NODE_DREGX_ONCE 153 | NODE_ARGS, 154 | #define NODE_ARGS NODE_ARGS 155 | NODE_ARGS_AUX, 156 | #define NODE_ARGS_AUX NODE_ARGS_AUX 157 | NODE_OPT_ARG, 158 | #define NODE_OPT_ARG NODE_OPT_ARG 159 | NODE_KW_ARG, 160 | #define NODE_KW_ARG NODE_KW_ARG 161 | NODE_POSTARG, 162 | #define NODE_POSTARG NODE_POSTARG 163 | NODE_ARGSCAT, 164 | #define NODE_ARGSCAT NODE_ARGSCAT 165 | NODE_ARGSPUSH, 166 | #define NODE_ARGSPUSH NODE_ARGSPUSH 167 | NODE_SPLAT, 168 | #define NODE_SPLAT NODE_SPLAT 169 | NODE_TO_ARY, 170 | #define NODE_TO_ARY NODE_TO_ARY 171 | NODE_BLOCK_ARG, 172 | #define NODE_BLOCK_ARG NODE_BLOCK_ARG 173 | NODE_BLOCK_PASS, 174 | #define NODE_BLOCK_PASS NODE_BLOCK_PASS 175 | NODE_DEFN, 176 | #define NODE_DEFN NODE_DEFN 177 | NODE_DEFS, 178 | #define NODE_DEFS NODE_DEFS 179 | NODE_ALIAS, 180 | #define NODE_ALIAS NODE_ALIAS 181 | NODE_VALIAS, 182 | #define NODE_VALIAS NODE_VALIAS 183 | NODE_UNDEF, 184 | #define NODE_UNDEF NODE_UNDEF 185 | NODE_CLASS, 186 | #define NODE_CLASS NODE_CLASS 187 | NODE_MODULE, 188 | #define NODE_MODULE NODE_MODULE 189 | NODE_SCLASS, 190 | #define NODE_SCLASS NODE_SCLASS 191 | NODE_COLON2, 192 | #define NODE_COLON2 NODE_COLON2 193 | NODE_COLON3, 194 | #define NODE_COLON3 NODE_COLON3 195 | NODE_DOT2, 196 | #define NODE_DOT2 NODE_DOT2 197 | NODE_DOT3, 198 | #define NODE_DOT3 NODE_DOT3 199 | NODE_FLIP2, 200 | #define NODE_FLIP2 NODE_FLIP2 201 | NODE_FLIP3, 202 | #define NODE_FLIP3 NODE_FLIP3 203 | NODE_SELF, 204 | #define NODE_SELF NODE_SELF 205 | NODE_NIL, 206 | #define NODE_NIL NODE_NIL 207 | NODE_TRUE, 208 | #define NODE_TRUE NODE_TRUE 209 | NODE_FALSE, 210 | #define NODE_FALSE NODE_FALSE 211 | NODE_ERRINFO, 212 | #define NODE_ERRINFO NODE_ERRINFO 213 | NODE_DEFINED, 214 | #define NODE_DEFINED NODE_DEFINED 215 | NODE_POSTEXE, 216 | #define NODE_POSTEXE NODE_POSTEXE 217 | NODE_ALLOCA, 218 | #define NODE_ALLOCA NODE_ALLOCA 219 | NODE_BMETHOD, 220 | #define NODE_BMETHOD NODE_BMETHOD 221 | NODE_DSYM, 222 | #define NODE_DSYM NODE_DSYM 223 | NODE_ATTRASGN, 224 | #define NODE_ATTRASGN NODE_ATTRASGN 225 | NODE_PRELUDE, 226 | #define NODE_PRELUDE NODE_PRELUDE 227 | NODE_LAMBDA, 228 | #define NODE_LAMBDA NODE_LAMBDA 229 | NODE_LAST 230 | #define NODE_LAST NODE_LAST 231 | }; 232 | 233 | typedef struct RNode { 234 | VALUE flags; 235 | VALUE nd_reserved; /* ex nd_file */ 236 | union { 237 | struct RNode *node; 238 | ID id; 239 | VALUE value; 240 | VALUE (*cfunc)(ANYARGS); 241 | ID *tbl; 242 | } u1; 243 | union { 244 | struct RNode *node; 245 | ID id; 246 | long argc; 247 | VALUE value; 248 | } u2; 249 | union { 250 | struct RNode *node; 251 | ID id; 252 | long state; 253 | struct rb_global_entry *entry; 254 | struct rb_args_info *args; 255 | long cnt; 256 | VALUE value; 257 | } u3; 258 | } NODE; 259 | 260 | #define RNODE(obj) (R_CAST(RNode)(obj)) 261 | 262 | /* FL : 0..4: T_TYPES, 5: KEEP_WB, 6: PROMOTED, 7: FINALIZE, 8: TAINT, 9: UNTRUSTERD, 10: EXIVAR, 11: FREEZE */ 263 | /* NODE_FL: 0..4: T_TYPES, 5: KEEP_WB, 6: PROMOTED, 7: NODE_FL_NEWLINE|NODE_FL_CREF_PUSHED_BY_EVAL, 264 | * 8..14: nd_type, 265 | * 15..: nd_line or 266 | * 15: NODE_FL_CREF_PUSHED_BY_EVAL 267 | * 16: NODE_FL_CREF_OMOD_SHARED 268 | */ 269 | #define NODE_FL_NEWLINE (((VALUE)1)<<7) 270 | 271 | #define NODE_TYPESHIFT 8 272 | #define NODE_TYPEMASK (((VALUE)0x7f)<flags & NODE_TYPEMASK)>>NODE_TYPESHIFT)) 275 | #define nd_set_type(n,t) \ 276 | RNODE(n)->flags=((RNODE(n)->flags&~NODE_TYPEMASK)|((((unsigned long)(t))<flags>>NODE_LSHIFT) 281 | #define nd_set_line(n,l) \ 282 | RNODE(n)->flags=((RNODE(n)->flags&~((VALUE)(-1)<