detective 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.rdoc CHANGED
@@ -1,18 +1,74 @@
1
1
  = detective
2
2
 
3
- Description goes here.
4
-
5
- == Note on Patches/Pull Requests
6
-
7
- * Fork the project.
8
- * Make your feature addition or bug fix.
9
- * Add tests for it. This is important so I don't break it in a
10
- future version unintentionally.
11
- * Commit, do not mess with rakefile, version, or history.
12
- (if you want to have your own version, that is fine but
13
- bump version in a commit by itself I can ignore when I pull)
14
- * Send me a pull request. Bonus points for topic branches.
3
+ Detective is a gem build by BadrIT (http://www.badrit.com/) to investigate the ruby source code.
4
+ Check the examples below.
5
+
6
+ == Motivation
7
+ Tired of opening files of installed gems to know how the code is working?
8
+ Not able to know who and where that function has been overrided?
9
+ It's time to get help from a private Detective.
10
+ Detective allows you show the source and find the location of some ruby method.
11
+
12
+ == Examples
13
+ View the source of a class method ...
14
+ require 'detective'
15
+
16
+ Detective.view_source('ActiveRecord::Base.find_by_sql')
17
+
18
+ def find_by_sql(sql)
19
+ connection.select_all(sanitize_sql(sql), "#{name} Load").collect! { |record| instantiate(record) }
20
+ end
21
+
22
+ View the source of an instance method ...
23
+
24
+ Detective.view_source('ActiveRecord::Base#update_attributes')
25
+
26
+ def update_attributes(attributes)
27
+ self.attributes = attributes
28
+ save
29
+ end
30
+
31
+ View the source of an overrided method ...
32
+ ActiveRecord::Base.class_eval do
33
+ def update_attributes(attributes)
34
+ puts "updating attributes ..."
35
+ self.attributes = attributes
36
+ save
37
+ end
38
+ end
39
+
40
+ Detective.view_source('ActiveRecord::Base#update_attributes')
41
+
42
+ def update_attributes(attributes)
43
+ puts "updating attributes ..."
44
+ self.attributes = attributes
45
+ save
46
+ end
47
+
48
+ Find the location of some source ...
49
+ Detective.get_location('ActiveRecord::Base#attributes')
50
+
51
+ location
52
+ /home/aseldawy/.gem/ruby/1.8/gems/activerecord-2.3.4/lib/active_record/base.rb
53
+ 2752
54
+
55
+ No luck with native methods ...
56
+ Detective.view_source('String#length')
57
+
58
+ native method
59
+
60
+ == How it works (advanced)
61
+ The idea is to invoke the given method and trace the execution of the program.
62
+ This allows us to know where is the definition of the method.
63
+ Then with the help of RubyParser, we can extract its code from the file.
64
+ The invoke of this method is made in a separate process so that it doesn't conflict with your program.
65
+ This child process is killed before the method starts its execution so the method is not really invoked.
66
+
67
+ For systems not supporting fork (like windows), the child process is replaced with a thread.
68
+ This might make some problems because it is running in the same space of your own ruby program.
69
+ We make our best to decrease the effect of this call by killing the thread before the method starts execution.
70
+ However, given that Detective will be used while developing only, we can ignore this effect.
15
71
 
16
72
  == Copyright
17
73
 
18
- Copyright (c) 2009 Ahmed ElDawy. See LICENSE for details.
74
+ Copyright (c) 2009 BadrIT. See LICENSE for details.
data/Rakefile CHANGED
@@ -11,7 +11,8 @@ begin
11
11
  gem.homepage = "http://github.com/aseldawy/detective"
12
12
  gem.authors = ["Ahmed ElDawy"]
13
13
  gem.rubyforge_project = "detective"
14
- gem.add_development_dependency "minitest", ">= 0"
14
+ gem.add_dependency('ruby_parser')
15
+
15
16
  # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
17
  end
17
18
  Jeweler::GemcutterTasks.new
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.0
1
+ 0.1.0
data/detective.gemspec ADDED
@@ -0,0 +1,55 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{detective}
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Ahmed ElDawy"]
12
+ s.date = %q{2009-11-20}
13
+ s.description = %q{A gem that allows you to view the source code of a method}
14
+ s.email = %q{ahmed.eldawy@badrit.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ "LICENSE",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "detective.gemspec",
27
+ "lib/detective.rb",
28
+ "test/helper.rb",
29
+ "test/test_detective.rb"
30
+ ]
31
+ s.homepage = %q{http://github.com/aseldawy/detective}
32
+ s.rdoc_options = ["--charset=UTF-8"]
33
+ s.require_paths = ["lib"]
34
+ s.rubyforge_project = %q{detective}
35
+ s.rubygems_version = %q{1.3.5}
36
+ s.summary = %q{Find source code of ruby methods}
37
+ s.test_files = [
38
+ "test/helper.rb",
39
+ "test/test_detective.rb"
40
+ ]
41
+
42
+ if s.respond_to? :specification_version then
43
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
44
+ s.specification_version = 3
45
+
46
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
47
+ s.add_runtime_dependency(%q<ruby_parser>, [">= 0"])
48
+ else
49
+ s.add_dependency(%q<ruby_parser>, [">= 0"])
50
+ end
51
+ else
52
+ s.add_dependency(%q<ruby_parser>, [">= 0"])
53
+ end
54
+ end
55
+
data/lib/detective.rb CHANGED
@@ -0,0 +1,173 @@
1
+ require 'ruby_parser'
2
+
3
+ module Detective
4
+
5
+ ForkSupported = respond_to? :fork
6
+
7
+ def self.view_source(method)
8
+ location = get_location(method).strip.split /[\r\n]+/
9
+ case location.first
10
+ when 'native method' then return 'native method'
11
+ when 'error' then raise location[1..-1].join(' ')
12
+ when 'location' then
13
+ begin
14
+ filename, line_no = location[1,2]
15
+ line_no = line_no.to_i
16
+ f = File.open filename
17
+ source = ""
18
+ file_line_no = 0
19
+ rp = RubyParser.new
20
+ f.each_line do |file_line|
21
+ file_line_no += 1
22
+ if file_line_no >= line_no
23
+ source << file_line
24
+ # Try to parse it to know whether the method is complete or not
25
+ rp.parse(source) && break rescue nil
26
+ end
27
+ end
28
+ f.close
29
+ return source
30
+ rescue
31
+ return "Cannot find source code"
32
+ end
33
+ end
34
+ end
35
+
36
+ # Finds the location of a method in ruby source files
37
+ # You can pass a string like
38
+ # * 'Class.name' class method
39
+ # # 'String#size' instance method
40
+ def self.get_location(ruby_statement)
41
+ if ruby_statement.index('#')
42
+ # instance method
43
+ class_name, method_name = ruby_statement.split('#')
44
+ class_method = false
45
+ elsif ruby_statement.index('.')
46
+ class_name, method_name = ruby_statement.split('.')
47
+ class_method = true
48
+ else
49
+ raise "Invalid parameter"
50
+ end
51
+ the_klass = eval(class_name)
52
+ ForkSupported ? get_location_fork(the_klass, method_name, class_method) : get_location_thread(the_klass, method_name, class_method)
53
+ end
54
+
55
+ private
56
+
57
+ def self.get_location_thread(the_klass, method_name, class_method)
58
+ if class_method
59
+ raise "Invalid class method name #{method_name} for class #{the_klass}" unless the_klass.respond_to? method_name
60
+ else
61
+ raise "Invalid instance method name #{method_name} for class #{the_klass}" unless the_klass.instance_methods.include? method_name
62
+ end
63
+ result = ""
64
+ t = Thread.new do
65
+ # child process
66
+ detective_state = 0
67
+ # Get an instance of class Method that can be invoked using Method#call
68
+ the_method, args = get_method(the_klass, method_name, class_method)
69
+ set_trace_func(proc do |event, file, line, id, binding, classname|
70
+ if id == :call
71
+ detective_state = 1
72
+ return
73
+ end
74
+ return if detective_state == 0
75
+ if event == 'call'
76
+ result << "location" << "\r\n"
77
+ result << file << "\r\n"
78
+ result << line.to_s << "\r\n"
79
+ # Cancel debugging
80
+ set_trace_func nil
81
+ Thread.kill(Thread.current)
82
+ elsif event == 'c-call'
83
+ result << 'native method'
84
+ set_trace_func nil
85
+ Thread.kill(Thread.current)
86
+ end
87
+ end)
88
+
89
+ begin
90
+ the_method.call *args
91
+ # If the next line executed, this indicates an error because the method should be cancelled before called
92
+ result << "method called!" << "\r\n"
93
+ rescue Exception => e
94
+ result << "error" << "\r\n"
95
+ result << e.inspect << "\r\n"
96
+ end
97
+ end
98
+ t.join
99
+ result
100
+ end
101
+
102
+ def self.get_location_fork(the_klass, method_name, class_method)
103
+ f = open("|-", "w+")
104
+ if f == nil
105
+ # child process
106
+ detective_state = 0
107
+ # Get an instance of class Method that can be invoked using Method#call
108
+ the_method, args = get_method(the_klass, method_name, class_method)
109
+ set_trace_func(proc do |event, file, line, id, binding, classname|
110
+ if id == :call
111
+ detective_state = 1
112
+ return
113
+ end
114
+ return if detective_state == 0
115
+ if event == 'call'
116
+ puts "location"
117
+ puts file
118
+ puts line
119
+ set_trace_func nil
120
+ exit!
121
+ elsif event == 'c-call'
122
+ puts 'native method'
123
+ set_trace_func nil
124
+ exit!
125
+ end
126
+ end)
127
+
128
+ begin
129
+ the_method.call *args
130
+ # If the next line executed, this indicates an error because the method should be cancelled before called
131
+ puts "method called!"
132
+ rescue => e
133
+ puts "error"
134
+ puts e.inspect
135
+ ensure
136
+ exit!
137
+ end
138
+ else
139
+ Process.wait
140
+ x = f.read
141
+ # puts x
142
+ return x
143
+ end
144
+ end
145
+
146
+ def self.get_method(the_klass, method_name, class_method)
147
+ if class_method
148
+ the_method = the_klass.method(method_name)
149
+ else
150
+ # Create a new empty initialize method to bypass initialization
151
+ the_klass.class_eval do
152
+ alias old_initialize initialize
153
+ def initialize
154
+ # Bypass initialization
155
+ end
156
+ end
157
+ the_method = the_klass.new.method(method_name)
158
+ # Revert initialize method
159
+ the_klass.class_eval do
160
+ # under causes a warning
161
+ # undef initialize
162
+ alias initialize old_initialize
163
+ end
164
+ end
165
+ # check how many attributes are required
166
+ the_method_arity = the_method.arity
167
+ required_args = the_method_arity < 0 ? -the_method_arity-1 : the_method_arity
168
+
169
+ # Return the method and its parameters
170
+ [the_method, Array.new(required_args)]
171
+ end
172
+
173
+ end
data/test/helper.rb CHANGED
@@ -1,11 +1,9 @@
1
1
  require 'rubygems'
2
- require 'minitest/unit'
2
+ require 'test/unit'
3
3
 
4
4
  $LOAD_PATH.unshift(File.dirname(__FILE__))
5
5
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
6
  require 'detective'
7
7
 
8
- class MiniTest::Unit::TestCase
8
+ class Test::Unit::TestCase
9
9
  end
10
-
11
- MiniTest::Unit.autorun
@@ -1,7 +1,141 @@
1
1
  require 'helper'
2
2
 
3
- class TestDetective < MiniTest::Unit::TestCase
4
- def test_something_for_real
5
- flunk "hey buddy, you should probably rename this file and start testing for real"
3
+ class BadrIT; end
4
+
5
+ class TestDetective < Test::Unit::TestCase
6
+ def test_simple_method
7
+ BadrIT.class_eval do
8
+ def self.hello
9
+ puts "hello BadrIT"
10
+ end
11
+ end
12
+
13
+ source = Detective.view_source('BadrIT.hello')
14
+ assert_equal 'def self.hello puts "hello BadrIT" end', source.gsub(/\s+/, ' ').strip
15
+ end
16
+
17
+ def test_overrided_method
18
+ def BadrIT.abc
19
+ puts "BadrIT rulez!!"
20
+ end
21
+
22
+ source = Detective.view_source('BadrIT.abc')
23
+ assert_equal 'def BadrIT.abc puts "BadrIT rulez!!" end', source.gsub(/\s+/, ' ').strip
24
+
25
+ def BadrIT.abc
26
+ puts "BadrIT is the best!!"
27
+ end
28
+
29
+ source = Detective.view_source('BadrIT.abc')
30
+ assert_equal 'def BadrIT.abc puts "BadrIT is the best!!" end', source.gsub(/\s+/, ' ').strip
31
+ end
32
+
33
+ def test_instance_method
34
+ BadrIT.class_eval do
35
+ def test
36
+ puts "testing"
37
+ end
38
+ end
39
+
40
+ source = Detective.view_source('BadrIT#test')
41
+ assert_equal 'def test puts "testing" end', source.gsub(/\s+/, ' ').strip
42
+ end
43
+
44
+ def test_method_with_args
45
+ BadrIT.class_eval do
46
+ def test(arg0)
47
+ puts "nothing here"
48
+ end
49
+ end
50
+
51
+ source = Detective.view_source('BadrIT#test')
52
+ assert_equal 'def test(arg0) puts "nothing here" end', source.gsub(/\s+/, ' ').strip
53
+ end
54
+
55
+ def test_method_with_optional_args
56
+ BadrIT.class_eval do
57
+ def test1(arg0, arg1="test", arg2="habal")
58
+ puts "nothing here"
59
+ end
60
+ end
61
+
62
+ source = Detective.view_source('BadrIT#test1')
63
+ assert_equal 'def test1(arg0, arg1="test", arg2="habal") puts "nothing here" end', source.gsub(/\s+/, ' ').strip
64
+ end
65
+
66
+ def test_method_with_variable_args
67
+ BadrIT.class_eval do
68
+ def test2(arg0, arg1="test", *args)
69
+ puts "nothing here"
70
+ end
71
+ end
72
+
73
+ source = Detective.view_source('BadrIT#test2')
74
+ assert_equal 'def test2(arg0, arg1="test", *args) puts "nothing here" end', source.gsub(/\s+/, ' ').strip
75
+ end
76
+
77
+ def test_native_method
78
+ source = Detective.view_source('String#length')
79
+ assert_equal 'native method', source.gsub(/\s+/, ' ').strip
80
+ end
81
+
82
+ def test_native_method_with_args
83
+ source = Detective.view_source('String#sub')
84
+ assert_equal 'native method', source.gsub(/\s+/, ' ').strip
85
+ end
86
+
87
+ def test_undefined_method
88
+ assert_raises RuntimeError do
89
+ Detective.view_source('String#adfasdf')
90
+ end
91
+ end
92
+
93
+ def test_using_threads
94
+ fork_supported = Detective.const_get(:ForkSupported)
95
+ Detective.const_set(:ForkSupported, false)
96
+ BadrIT.class_eval do
97
+ def self.noway
98
+ puts "Go away"
99
+ end
100
+ end
101
+
102
+ source = Detective.view_source('BadrIT.noway')
103
+ assert_equal 'def self.noway puts "Go away" end', source.gsub(/\s+/, ' ').strip
104
+
105
+ Detective.const_set(:ForkSupported, fork_supported)
6
106
  end
107
+
108
+ def test_with_required_initializers
109
+ BadrIT.class_eval do
110
+ def initialize(arg0, arg1, arg2)
111
+ end
112
+
113
+ def some_method
114
+ puts "me myself"
115
+ end
116
+ end
117
+
118
+ source = Detective.view_source('BadrIT#some_method')
119
+ assert_equal 'def some_method puts "me myself" end', source.gsub(/\s+/, ' ').strip
120
+ end
121
+
122
+ def test_method_with_eval
123
+ eval %Q{
124
+ BadrIT.class_eval do
125
+ def self.yaya
126
+ puts "yaya BadrIT"
127
+ end
128
+ end
129
+ }
130
+
131
+ source = Detective.view_source('BadrIT.yaya')
132
+ assert_equal 'Cannot find source code', source.gsub(/\s+/, ' ').strip
133
+ end
134
+
135
+ def test_invalid_method_name
136
+ assert_raises RuntimeError do
137
+ Detective.view_source('BadrIT')
138
+ end
139
+ end
140
+
7
141
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: detective
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ahmed ElDawy
@@ -9,12 +9,12 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-11-19 00:00:00 +02:00
12
+ date: 2009-11-20 00:00:00 +02:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
- name: minitest
17
- type: :development
16
+ name: ruby_parser
17
+ type: :runtime
18
18
  version_requirement:
19
19
  version_requirements: !ruby/object:Gem::Requirement
20
20
  requirements:
@@ -38,6 +38,7 @@ files:
38
38
  - README.rdoc
39
39
  - Rakefile
40
40
  - VERSION
41
+ - detective.gemspec
41
42
  - lib/detective.rb
42
43
  - test/helper.rb
43
44
  - test/test_detective.rb