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 +69 -13
- data/Rakefile +2 -1
- data/VERSION +1 -1
- data/detective.gemspec +55 -0
- data/lib/detective.rb +173 -0
- data/test/helper.rb +2 -4
- data/test/test_detective.rb +137 -3
- metadata +5 -4
data/README.rdoc
CHANGED
@@ -1,18 +1,74 @@
|
|
1
1
|
= detective
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
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.
|
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.
|
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 '
|
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
|
8
|
+
class Test::Unit::TestCase
|
9
9
|
end
|
10
|
-
|
11
|
-
MiniTest::Unit.autorun
|
data/test/test_detective.rb
CHANGED
@@ -1,7 +1,141 @@
|
|
1
1
|
require 'helper'
|
2
2
|
|
3
|
-
class
|
4
|
-
|
5
|
-
|
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.
|
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-
|
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:
|
17
|
-
type: :
|
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
|