detective 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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