rubydeps 0.2.0 → 0.9.0.pre

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -17,5 +17,7 @@ tmtags
17
17
  coverage
18
18
  rdoc
19
19
  pkg
20
+ tmp
21
+ *.svg
20
22
 
21
23
  ## PROJECT::SPECIFIC
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ before_install:
3
+ - sudo apt-get update
4
+ - sudo apt-get install graphviz
5
+ rvm:
6
+ - 1.9.3
7
+ - 1.9.2
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem 'rake-compiler', "~> 0.8.0"
4
+ gem 'rspec', "~> 2.8.0"
5
+ gem 'file_test_helper', "~> 1.0.0"
6
+ gem 'ruby_core_source', "~> 0.1.5"
7
+ gem 'ruby-graphviz', "~> 1.0.5"
8
+ gem 'thor', "~> 0.14.2"
data/Gemfile.lock ADDED
@@ -0,0 +1,32 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ archive-tar-minitar (0.5.2)
5
+ diff-lcs (1.1.3)
6
+ file_test_helper (1.0.2)
7
+ rake (0.9.2.2)
8
+ rake-compiler (0.8.0)
9
+ rake
10
+ rspec (2.8.0)
11
+ rspec-core (~> 2.8.0)
12
+ rspec-expectations (~> 2.8.0)
13
+ rspec-mocks (~> 2.8.0)
14
+ rspec-core (2.8.0)
15
+ rspec-expectations (2.8.0)
16
+ diff-lcs (~> 1.1.2)
17
+ rspec-mocks (2.8.0)
18
+ ruby-graphviz (1.0.5)
19
+ ruby_core_source (0.1.5)
20
+ archive-tar-minitar (>= 0.5.2)
21
+ thor (0.14.6)
22
+
23
+ PLATFORMS
24
+ ruby
25
+
26
+ DEPENDENCIES
27
+ file_test_helper (~> 1.0.0)
28
+ rake-compiler (~> 0.8.0)
29
+ rspec (~> 2.8.0)
30
+ ruby-graphviz (~> 1.0.5)
31
+ ruby_core_source (~> 0.1.5)
32
+ thor (~> 0.14.2)
data/README.md CHANGED
@@ -1,8 +1,21 @@
1
+ [![Build Status](https://secure.travis-ci.org/dcadenas/rubydeps.png?branch=master)](http://travis-ci.org/dcadenas/rubydeps)
1
2
  rubydeps
2
3
  ========
3
4
 
4
5
  A tool to create class dependency graphs from test suites
5
6
 
7
+ Sample output
8
+ -------------
9
+
10
+ This is the result of running rubydeps on the [Rake](https://github.com/jimweirich/rake) tests:
11
+
12
+ ```bash
13
+ rubydeps testunit --class_name_filter='^Rake'
14
+ ```
15
+
16
+ ![Rake dependencies](https://github.com/dcadenas/rubydeps/raw/master/rake-deps.png)
17
+
18
+
6
19
  Command line usage
7
20
  ------------------
8
21
 
@@ -12,51 +25,78 @@ Rubydeps will run your test suite to record the call graph of your project and u
12
25
  First of all, be sure to step into the root directory of your project, rubydeps searches for ./spec or ./test dirs from there.
13
26
  For example, if we want to graph the Rails activemodel dependency graph we'd cd to rails/activemodel and from there we'd write:
14
27
 
15
- rubydeps testunit #to run Test::Unit tests
28
+ ```bash
29
+ rubydeps testunit #to run Test::Unit tests
30
+ ```
31
+
16
32
  or
17
- rubydeps rspec #to run RSpec tests
33
+
34
+ ```bash
35
+ rubydeps rspec #to run RSpec tests
36
+ ```
37
+
18
38
  or
19
- rubydeps rspec2 #to run RSpec 2 tests
39
+
40
+ ```bash
41
+ rubydeps rspec2 #to run RSpec 2 tests
42
+ ```
20
43
 
21
44
  This will output a rubydeps.dot. You can convert the dot file to any image format you like using the dot utility that comes with the graphviz installation e.g.:
22
45
 
23
- dot -Tsvg rubydeps.dot > rubydeps.svg
46
+ ```bash
47
+ dot -Tsvg rubydeps.dot > rubydeps.svg
48
+ ```
24
49
 
25
- ### Command line options
50
+ Notice that sometimes you may have missing dependencies as we graph the dependencies exercised by your tests so it's a quick bird's eye view to check your project coverage.
26
51
 
27
- The --path-filter option specifies a regexp that matches the path of the files you are interested in analyzing. For example you could have filters like 'project_name/app|project_name/lib' to analyze only code that is located in the 'app' and 'lib' dirs or as an alternative you could just exclude some directory you are not interested using a negative regexp like 'project_name(?!.*test)'
52
+ ### Command line options
28
53
 
29
- The --class_name_filter option is similar to the --path_filter options except that the regexp is matched against the class names (i.e. graph node names).
54
+ The `--path_filter` option specifies a regexp that matches the path of the files you are interested in analyzing. For example you could have filters like `'project_name/app|project_name/lib'` to analyze only code that is located in the `app` and `lib` dirs or as an alternative you could just exclude some directory you are not interested using a negative regexp like `'project_name(?!.*test)'`
30
55
 
31
- Library usage
32
- -------------
56
+ The `--class_name_filter` option is similar to the `--path_filter` options except that the regexp is matched against the class names (i.e. graph node names).
33
57
 
34
- Just require rubydeps and pass a block to analyze to the dot_for method.
58
+ The `--to_file` option dumps the dependency graph data to a file so you can do filtering later, it does not create a dot file.
35
59
 
36
- require 'rubydeps'
60
+ The `--from_file` option is only available when you don't specify a test command. Its argument is the file dumped through `--to_file` in a previous run. When you use this option the tests (or block) are not ran, the dependency graph is loaded directly from the file. This is useful to avoid rerunning code that didn't change just for the purpose of filtering with different combinations e.g.:
37
61
 
38
- Rubydeps.dot_for(:path_filter => path_filter_regexp, :class_name_filter) do
39
- //your code goes here
40
- end
62
+ ```bash
63
+ rubydeps rspec2 --to_file='dependencies.dump'
64
+ rubydeps --from_file='dependencies.dump' --path_filter='app/models'
65
+ rubydeps --from_file='dependencies.dump' --path_filter='app/models|app/controllers'
66
+ ```
41
67
 
42
- Sample output
68
+ Library usage
43
69
  -------------
44
70
 
45
- This is the result of running rubydeps on the [Mechanize](http://github.com/tenderlove/mechanize) tests:
71
+ Just require rubydeps and pass a block to analyze to the `analyze` method.
46
72
 
47
- ![Mechanize dependencies](https://github.com/dcadenas/rubydeps/raw/master/mechanize-deps.png)
73
+ ```ruby
74
+ require 'rubydeps'
48
75
 
49
- Notice that sometimes you may have missing dependencies as we graph the dependencies exercised by your tests so it's a quick bird's eye view to check your project coverage.
76
+ Rubydeps.analyze(:path_filter => path_filter_regexp, :class_name_filter => class_name_filter_regexp, :to_file => "dependencies.dump") do
77
+ # your code goes here
78
+ end
79
+ ```
50
80
 
51
81
  Installation
52
82
  ------------
53
83
 
54
- gem install rubydeps
84
+ ```bash
85
+ gem install rubydeps
86
+ ```
87
+
88
+ Rubydeps now only supports ruby 1.9. If you need 1.8.x support then:
89
+
90
+ ```bash
91
+ gem install rubydeps -v0.2.0
92
+ ```
93
+
94
+ Notice that in 0.2.0 you should use dot_for instead of analyze.
55
95
 
56
96
  Dependencies
57
97
  ------------
58
98
 
59
- * rcov
99
+ * rcov (only for version 0.2.0)
60
100
  * graphviz
61
101
  * ruby-graphviz
62
102
 
@@ -74,6 +114,6 @@ Note on Patches/Pull Requests
74
114
  Copyright
75
115
  ---------
76
116
 
77
- Copyright (c) 2010 Daniel Cadenas. See LICENSE for details.
117
+ Copyright (c) 2012 Daniel Cadenas. See LICENSE for details.
78
118
 
79
- Development sponsored by [Cubox](http://www.cuboxsa.com)
119
+ Development sponsored by [Cubox](http://www.cuboxlabs.com)
data/Rakefile ADDED
@@ -0,0 +1,39 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ spec = Gem::Specification.new do |s|
5
+ s.extensions = FileList["ext/**/extconf.rb"]
6
+ s.name = "rubydeps"
7
+ s.summary = %Q{A tool to create class dependency graphs from test suites}
8
+ s.description = %Q{A tool to create class dependency graphs from test suites}
9
+ s.email = "dcadenas@gmail.com"
10
+ s.homepage = "http://github.com/dcadenas/rubydeps"
11
+ s.authors = ["Daniel Cadenas"]
12
+ s.executables = ["rubydeps"]
13
+
14
+ s.add_development_dependency(%q<rake-compiler>, ["~> 0.8.0"])
15
+ s.add_development_dependency(%q<rspec>, ["~> 2.8.0"])
16
+ s.add_development_dependency(%q<file_test_helper>, ["~> 1.0.0"])
17
+ s.add_dependency(%q<ruby_core_source>, ["~> 0.1.5"])
18
+ s.add_dependency(%q<ruby-graphviz>, ["~> 1.0.5"])
19
+ s.add_dependency(%q<thor>, ["~> 0.14.2"])
20
+
21
+ s.version = File.read("VERSION")
22
+ s.files = `git ls-files`.split
23
+ end
24
+
25
+ require 'rake/extensiontask'
26
+ Gem::PackageTask.new(spec) do |pkg|
27
+ end
28
+
29
+ Rake::ExtensionTask.new('call_site_analyzer', spec)
30
+
31
+ require 'rspec/core/rake_task'
32
+ RSpec::Core::RakeTask.new(:spec) do |t|
33
+ t.rspec_opts = ["-f progress", "-r ./spec/spec_helper.rb"]
34
+ t.pattern = 'spec/*_spec.rb'
35
+ end
36
+
37
+ task :spec => :compile
38
+
39
+ task :default => :spec
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.9.0.pre
data/bin/rubydeps CHANGED
@@ -5,19 +5,28 @@ require 'thor'
5
5
  module Rubydeps
6
6
  class Runner < Thor
7
7
  desc "testunit", "Create the dependency graph after runnning the testunit tests"
8
+ method_option :to_file, :type => :string
8
9
  method_option :path_filter, :type => :string, :default => `pwd`.chomp, :required => true
9
10
  method_option :class_name_filter, :type => :string, :default => '', :required => true
10
11
  def testunit
11
- require 'test/unit'
12
+ require 'minitest/unit'
13
+ require 'test/unit/assertions'
14
+ require 'test/unit/testcase'
15
+
16
+ $LOAD_PATH.unshift("#{`pwd`.chomp}/lib")
17
+
18
+ #dirty hack so that minitest doesn't install the at_exit hook and we can run the tests in this same process
19
+ ::MiniTest::Unit.class_variable_set("@@installed_at_exit", true)
12
20
 
13
21
  (Dir["./test/**/*_test.rb"] + Dir["./test/**/test_*.rb"]).each { |f| load f unless f =~ /^-/ }
14
22
 
15
23
  create_dependencies_dot_for(options) do
16
- ::Test::Unit::AutoRunner.run
24
+ ::MiniTest::Unit.new.run([])
17
25
  end
18
26
  end
19
27
 
20
28
  desc "rspec", "Create the dependency graph after runnning the rspec tests"
29
+ method_option :to_file, :type => :string
21
30
  #TODO: this breaks when using underscores, investigate
22
31
  method_option :path_filter, :type => :string, :default => `pwd`.chomp, :required => true
23
32
  method_option :class_name_filter, :type => :string, :default => '', :required => true
@@ -33,7 +42,17 @@ module Rubydeps
33
42
  end
34
43
  end
35
44
 
45
+ desc "", "Loads dependencies saved by a --to_file option in a previous run. Doesn't run tests"
46
+ method_option :from_file, :type => :string, :required => true
47
+ method_option :path_filter, :type => :string, :default => `pwd`.chomp, :required => true
48
+ method_option :class_name_filter, :type => :string, :default => '', :required => true
49
+ default_task :load_deps
50
+ def load_deps
51
+ puts options[:from_file]
52
+ end
53
+
36
54
  desc "rspec2", "Create the dependency graph after runnning the rspec 2 tests"
55
+ method_option :to_file, :type => :string
37
56
  method_option :path_filter, :type => :string, :default => `pwd`.chomp, :required => true
38
57
  method_option :class_name_filter, :type => :string, :default => '', :required => true
39
58
  def rspec2
@@ -47,11 +66,14 @@ module Rubydeps
47
66
  end
48
67
  end
49
68
 
50
- private
69
+ private
51
70
 
52
71
  def create_dependencies_dot_for(options)
53
72
  ARGV.clear
54
- Rubydeps.dot_for(:path_filter => Regexp.new(options[:path_filter]), :class_name_filter => Regexp.new(options[:class_name_filter])) do
73
+ Rubydeps.analyze(:path_filter => Regexp.new(options[:path_filter]),
74
+ :class_name_filter => Regexp.new(options[:class_name_filter]),
75
+ :to_file => options[:to_file],
76
+ :from_file => options[:from_file]) do
55
77
  yield
56
78
  end
57
79
  end
@@ -59,4 +81,3 @@ module Rubydeps
59
81
  end
60
82
 
61
83
  Rubydeps::Runner.start(ARGV)
62
-
@@ -0,0 +1,147 @@
1
+ #include <ruby.h>
2
+ #include <vm_core.h>
3
+ #include <iseq.h>
4
+
5
+ // Fix compile error in ruby 1.9.3
6
+ #ifdef RTYPEDDATA_DATA
7
+ #define ruby_current_thread ((rb_thread_t *)RTYPEDDATA_DATA(rb_thread_current()))
8
+ #endif
9
+
10
+ inline static rb_control_frame_t*
11
+ callsite_cfp(rb_control_frame_t* cfp){
12
+ cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp);
13
+ if (cfp->iseq != 0 && cfp->pc != 0) {
14
+ return cfp;
15
+ }
16
+ else if (cfp->block_iseq) {
17
+ while (!cfp->iseq) cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp);
18
+ return cfp;
19
+ }
20
+
21
+ return NULL;
22
+ }
23
+
24
+ inline static VALUE
25
+ get_real_class(VALUE klass){
26
+ if (FL_TEST(klass, FL_SINGLETON)) {
27
+ VALUE v = rb_iv_get(klass, "__attached__");
28
+
29
+ switch (TYPE(v)) {
30
+ case T_CLASS: case T_MODULE:
31
+ return v;
32
+ default:
33
+ return rb_class_real(klass);
34
+ }
35
+ }
36
+ return rb_class_real(klass);
37
+ }
38
+
39
+ inline static VALUE
40
+ class_of_obj_or_class(VALUE obj_or_class){
41
+ switch(TYPE(obj_or_class)){
42
+ case T_CLASS: case T_MODULE:
43
+ return obj_or_class;
44
+ default:
45
+ return rb_obj_class(obj_or_class);
46
+ }
47
+ }
48
+
49
+ static VALUE dependency_array;
50
+
51
+ inline static void
52
+ add_dependency(VALUE calling_class, VALUE called_class, VALUE called_class_file_path, VALUE is_guess){
53
+ if(called_class == calling_class){
54
+ return;
55
+ }
56
+
57
+ const char* called_class_name = rb_class2name(called_class);
58
+ const char* calling_class_name= rb_class2name(calling_class);
59
+
60
+ //update depedency_hash
61
+ VALUE dependency_hash = rb_ary_entry(dependency_array, 0);
62
+ VALUE calling_class_array = rb_hash_aref(dependency_hash, rb_str_new2(called_class_name));
63
+ if(NIL_P(calling_class_array)){
64
+ calling_class_array = rb_ary_new();
65
+ rb_hash_aset(dependency_hash, rb_str_new2(called_class_name), calling_class_array);
66
+ }
67
+ rb_ary_push(calling_class_array, rb_str_new2(calling_class_name));
68
+
69
+ //update class_location_hash
70
+ if(!NIL_P(called_class_file_path)){
71
+ VALUE class_location_hash = rb_ary_entry(dependency_array, 1);
72
+
73
+ VALUE file_path_array = rb_hash_aref(class_location_hash, rb_str_new2(called_class_name));
74
+ if(NIL_P(file_path_array)){
75
+ file_path_array = rb_ary_new();
76
+ rb_hash_aset(class_location_hash, rb_str_new2(called_class_name), file_path_array);
77
+ }
78
+
79
+ VALUE last_guess = rb_ary_entry(file_path_array, 1);
80
+ if(last_guess == Qnil || last_guess == Qtrue){
81
+ rb_ary_store(file_path_array, 0, called_class_file_path);
82
+ rb_ary_store(file_path_array, 1, is_guess);
83
+ }
84
+ }
85
+ }
86
+
87
+ //NOTE: this function should be as optimized as possible as it's being called on each ruby method call
88
+ static void
89
+ event_hook(rb_event_flag_t event, VALUE data, VALUE self, ID mid, VALUE klass){
90
+ rb_control_frame_t* cfp = GET_THREAD()->cfp;
91
+ VALUE class_of_called_object = class_of_obj_or_class(self);
92
+ VALUE called_class = get_real_class(cfp->iseq->klass);
93
+
94
+ rb_control_frame_t* previous_cfp = callsite_cfp(cfp);
95
+ if(previous_cfp != NULL){
96
+ VALUE calling_class = get_real_class(previous_cfp->iseq->klass);
97
+
98
+ if(class_of_called_object != calling_class){
99
+ if(class_of_called_object != called_class){
100
+ //we can't assume that the location of class_of_called_object is the same as called_class, so guess == true
101
+ add_dependency(calling_class, class_of_called_object, cfp->iseq->filepath, Qtrue);
102
+ } else {
103
+ add_dependency(calling_class, called_class, cfp->iseq->filepath, Qfalse);
104
+ }
105
+ }
106
+ }
107
+
108
+ //this dependency represents inheritance/inclusion/extension
109
+ if(class_of_called_object != called_class){
110
+ add_dependency(class_of_called_object, called_class, cfp->iseq->filepath, Qfalse);
111
+ }
112
+ }
113
+
114
+ static int uniq_calling_arrays(VALUE called_class, VALUE calling_class_array, VALUE extra){
115
+ rb_funcall(calling_class_array, rb_intern("uniq!"), 0);
116
+ return ST_CONTINUE;
117
+ }
118
+
119
+ static VALUE analyze(VALUE self){
120
+ if(rb_block_given_p()) {
121
+ dependency_array = rb_ary_new();
122
+ rb_global_variable(&dependency_array);
123
+
124
+ VALUE dependency_hash = rb_hash_new();
125
+ rb_ary_push(dependency_array, dependency_hash);
126
+
127
+ VALUE class_location_hash = rb_hash_new();
128
+ rb_ary_push(dependency_array, class_location_hash);
129
+
130
+ rb_add_event_hook(event_hook, RUBY_EVENT_CALL, Qnil);
131
+ rb_yield(Qnil);
132
+ rb_remove_event_hook(event_hook);
133
+
134
+ rb_hash_foreach(rb_ary_entry(dependency_array, 0), uniq_calling_arrays, 0);
135
+ } else {
136
+ rb_raise(rb_eArgError, "a block is required");
137
+ }
138
+
139
+ return dependency_array;
140
+ }
141
+
142
+ static VALUE rb_cCallSiteAnalyzer;
143
+
144
+ void Init_call_site_analyzer(){
145
+ rb_cCallSiteAnalyzer = rb_define_module("CallSiteAnalyzer");
146
+ rb_define_singleton_method(rb_cCallSiteAnalyzer, "analyze", analyze, 0);
147
+ }
@@ -0,0 +1,11 @@
1
+ require 'mkmf'
2
+ require "ruby_core_source"
3
+
4
+ #comment this line if not debugging
5
+ $CFLAGS='-ggdb -Wall -O0 -pipe'
6
+
7
+ hdrs = proc { have_header("vm_core.h") and have_header("iseq.h") }
8
+
9
+ if !Ruby_core_source::create_makefile_with_core(hdrs, "call_site_analyzer")
10
+ STDERR.print("Makefile creation failed\n")
11
+ end
Binary file
data/lib/rubydeps.rb CHANGED
@@ -1,99 +1,91 @@
1
1
  require 'graphviz'
2
2
  require 'set'
3
- require 'rcovrt'
4
- require 'rcov'
3
+ require 'call_site_analyzer'
5
4
 
6
5
  module Rubydeps
7
- def self.dot_for(options = {}, &block_to_analyze)
8
- dependencies_hash = hash_for(options, &block_to_analyze)
6
+ def self.analyze(options = {}, &block_to_analyze)
7
+ dependency_hash, class_location_hash = dependency_hash_for(options, &block_to_analyze)
9
8
 
10
- if dependencies_hash
11
- g = GraphViz::new( "G", :use => 'dot', :mode => 'major', :rankdir => 'LR', :concentrate => 'true', :fontname => 'Arial')
12
- dependencies_hash.each do |k,vs|
13
- if !k.empty? && !vs.empty?
14
- n1 = g.add_node(k.to_s)
15
- if vs.respond_to?(:each)
16
- vs.each do |v|
17
- unless v.empty?
18
- n2 = g.add_node(v.to_s)
19
- g.add_edge(n2, n1)
9
+ if options[:to_file]
10
+ File.open(options[:to_file], 'wb') do |f|
11
+ f.write Marshal.dump([dependency_hash, class_location_hash])
12
+ end
13
+ else
14
+ if dependency_hash
15
+ g = GraphViz::new( "G", :use => 'dot', :mode => 'major', :rankdir => 'LR', :concentrate => 'true', :fontname => 'Arial')
16
+ dependency_hash.each do |k,vs|
17
+ if !k.empty? && !vs.empty?
18
+ n1 = g.add_nodes(k.to_s)
19
+ if vs.respond_to?(:each)
20
+ vs.each do |v|
21
+ unless v.empty?
22
+ n2 = g.add_nodes(v.to_s)
23
+ g.add_edges(n2, n1)
24
+ end
20
25
  end
21
26
  end
22
27
  end
23
28
  end
24
- end
25
29
 
26
- g.output( :dot => "rubydeps.dot" )
30
+ g.output( :dot => "rubydeps.dot" )
31
+ end
27
32
  end
28
33
  end
29
34
 
30
- def self.hash_for(options = {}, &block_to_analyze)
31
- analyzer = Rcov::CallSiteAnalyzer.new
32
- analyzer.run_hooked do
33
- block_to_analyze.call
34
- end
35
+ def self.dependency_hash_for(options = {}, &block_to_analyze)
36
+ dependency_hash, class_location_hash = if options[:from_file]
37
+ Marshal.load(File.binread(options[:from_file]))
38
+ else
39
+ CallSiteAnalyzer.analyze(&block_to_analyze)
40
+ end
35
41
 
36
42
  path_filter = options.fetch(:path_filter, /.*/)
37
43
  class_name_filter = options.fetch(:class_name_filter, /.*/)
44
+ classes_to_remove = get_classes_to_remove(dependency_hash, class_location_hash, path_filter, class_name_filter)
38
45
 
39
- dependency_hash = create_dependency_hash(analyzer, path_filter)
40
- clean_hash(dependency_hash, class_name_filter)
41
- end
46
+ while(!classes_to_remove.empty?) do
47
+ klass_to_remove = classes_to_remove.pop
48
+ classes_calling_class_to_remove = dependency_hash[klass_to_remove]
49
+ classes_called_by_class_to_remove = dependency_hash.keys.select do |called_class|
50
+ dependency_hash[called_class].member? klass_to_remove
51
+ end
42
52
 
43
- private
44
- def self.path_filtered_site?(code_site, path_filter)
45
- code_site && path_filter =~ File.expand_path(code_site.file)
46
- end
53
+ #transitive dependencies, hmmm, not sure is a good idea
54
+ #if classes_calling_class_to_remove && !classes_calling_class_to_remove.empty?
55
+ # classes_called_by_class_to_remove.each do |called_class|
56
+ # dependency_hash[called_class] |= classes_calling_class_to_remove
57
+ # end
58
+ #end
47
59
 
48
- #we build a hash structured in this way: {"called_class_name1" => ["calling_class_name1", "calling_class_name2"], "called_class_name2" => ...}
49
- #TODO: want moar love here
50
- def self.create_dependency_hash(analyzer, path_filter)
51
- dependency_hash = {}
52
- analyzer.analyzed_classes.each do |c|
53
- called_class_name = normalize_class_name(c)
54
- analyzer.methods_for_class(c).each do |m|
55
- called_class_method = "#{c}##{m}"
56
- def_site = analyzer.defsite(called_class_method)
57
- if path_filtered_site?(def_site, path_filter)
58
- calling_class_names = Set.new
59
- analyzer.callsites(called_class_method).each do |call_site, _|
60
- if path_filtered_site?(call_site, path_filter)
61
- calling_class = call_site.calling_class
62
- calling_class_name = normalize_class_name(calling_class.to_s)
63
- calling_class_names << calling_class_name
64
- end
60
+ dependency_hash.delete(klass_to_remove)
61
+ classes_called_by_class_to_remove.each do |called_class|
62
+ if dependency_hash[called_class]
63
+ dependency_hash[called_class].delete(klass_to_remove)
64
+
65
+ if dependency_hash[called_class].empty?
66
+ dependency_hash.delete(called_class)
65
67
  end
66
- dependency_hash[called_class_name] ||= Set.new
67
- dependency_hash[called_class_name] += calling_class_names
68
68
  end
69
69
  end
70
+
70
71
  end
71
72
 
72
- dependency_hash
73
+ [normalize_class_names(dependency_hash), class_location_hash]
73
74
  end
74
75
 
75
- def self.clean_hash(dependency_hash, class_name_filter)
76
- cleaned_hash = {}
77
- dependency_hash.each do |called_class_name, calling_class_names|
78
- if interesting_class_name(called_class_name) && !dependency_hash[called_class_name].empty? && called_class_name =~ class_name_filter
79
- cleaned_hash[called_class_name] = (calling_class_names - [nil]).select do |c|
80
- interesting_class_name(c) &&
81
- c != called_class_name &&
82
- c =~ class_name_filter
83
- end
84
- cleaned_hash.delete(called_class_name) if cleaned_hash[called_class_name].empty?
85
- end
76
+ def self.get_classes_to_remove(dependency_hash, class_location_hash, path_filter, class_name_filter)
77
+ (dependency_hash.keys | dependency_hash.values.flatten).reject do |klass|
78
+ class_name_filter =~ klass &&
79
+ class_location_hash[klass] && !class_location_hash[klass].empty? && class_location_hash[klass].first =~ path_filter
86
80
  end
87
-
88
- cleaned_hash
89
81
  end
90
82
 
91
- def self.interesting_class_name(class_name)
92
- !class_name.empty? && class_name != "Rcov::CallSiteAnalyzer" && class_name != "Rcov::DifferentialAnalyzer"
83
+ def self.normalize_class_names(dependency_hash)
84
+ Hash[dependency_hash.map { |k,v| [normalize_class_name(k), v.map{|c| c == k ? nil : normalize_class_name(c)}.compact] }]
93
85
  end
94
86
 
95
87
  def self.normalize_class_name(klass)
96
- good_class_name = klass.gsub(/#<Class:(.+)>/, '\1')
88
+ good_class_name = klass.gsub(/#<(.+):(.+)>/, 'Instance of \1')
97
89
  good_class_name.gsub!(/\([^\)]*\)/, "")
98
90
  good_class_name.gsub(/0x[\da-fA-F]+/, '(hex number)')
99
91
  end
data/rake-deps.png ADDED
Binary file
data/rubydeps.gemspec ADDED
@@ -0,0 +1,57 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = %q{rubydeps}
3
+ s.version = "0.9.0.pre"
4
+
5
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
6
+ s.authors = ["Daniel Cadenas"]
7
+ s.date = %q{2010-09-28}
8
+ s.description = %q{Graphs ruby dependencies}
9
+ s.email = %q{dcadenas@gmail.com}
10
+ s.executables = ["rubydeps"]
11
+ s.extra_rdoc_files = [
12
+ "LICENSE"
13
+ ]
14
+ s.files = [
15
+ ".document",
16
+ ".gitignore",
17
+ "LICENSE",
18
+ "README.md",
19
+ "bin/rubydeps",
20
+ "lib/rubydeps.rb"
21
+ ]
22
+ s.homepage = %q{http://github.com/dcadenas/rubydeps}
23
+ s.rdoc_options = ["--charset=UTF-8"]
24
+ s.require_paths = ["lib"]
25
+ s.rubygems_version = %q{1.3.7}
26
+ s.summary = %q{Graphs ruby depencencies}
27
+ s.test_files = [
28
+ "spec/rubydeps_spec.rb",
29
+ "spec/spec_helper.rb"
30
+ ]
31
+
32
+ if s.respond_to? :specification_version then
33
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
34
+ s.specification_version = 3
35
+
36
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
37
+ s.add_development_dependency(%q<rake-compiler>, ["~> 0.8.0"])
38
+ s.add_development_dependency(%q<rspec>, ["~> 2.8.0"])
39
+ s.add_development_dependency(%q<file_test_helper>, ["~> 1.0.0"])
40
+ s.add_dependency(%q<ruby-graphviz>, ["~> 1.0.5"])
41
+ s.add_dependency(%q<thor>, ["~> 0.14.2"])
42
+ else
43
+ s.add_dependency(%q<rake-compiler>, ["~> 0.8.0"])
44
+ s.add_dependency(%q<rspec>, [">= 2.8.0"])
45
+ s.add_dependency(%q<file_test_helper>, ["~> 1.0.0"])
46
+ s.add_dependency(%q<ruby-graphviz>, ["~> 1.0.5"])
47
+ s.add_dependency(%q<thor>, ["~> 0.14.2"])
48
+ end
49
+ else
50
+ s.add_dependency(%q<rake-compiler>, ["~> 0.8.0"])
51
+ s.add_dependency(%q<rspec>, [">= 2.8.0"])
52
+ s.add_dependency(%q<file_test_helper>, ["~> 1.0.0"])
53
+ s.add_dependency(%q<ruby-graphviz>, ["~> 1.0.5"])
54
+ s.add_dependency(%q<thor>, ["~> 0.14.2"])
55
+ end
56
+ end
57
+
@@ -1,8 +1,12 @@
1
1
  require 'file_test_helper'
2
2
 
3
- class Grandparent
4
- def self.class_method
3
+ module GrandparentModule
4
+ def class_method
5
5
  end
6
+ end
7
+
8
+ class Grandparent
9
+ extend GrandparentModule
6
10
 
7
11
  def instance_method
8
12
  end
@@ -29,6 +33,14 @@ class Son
29
33
  def self.class_method2
30
34
  end
31
35
 
36
+ def instance_method_that_calls_parent_class_method
37
+ Parent.class_method
38
+ end
39
+
40
+ def instance_method_calling_another_instance_method(second_receiver)
41
+ second_receiver.instance_method
42
+ end
43
+
32
44
  def instance_method
33
45
  Parent.class_method
34
46
  Grandparent.class_method
@@ -38,7 +50,7 @@ end
38
50
  describe "Rubydeps" do
39
51
  include FileTestHelper
40
52
  it "should show the class level dependencies" do
41
- dependencies = Rubydeps.hash_for do
53
+ dependencies, _ = ::Rubydeps.dependency_hash_for do
42
54
  class IHaveAClassLevelDependency
43
55
  Son.class_method
44
56
  end
@@ -49,7 +61,7 @@ describe "Rubydeps" do
49
61
 
50
62
  it "should create a dot file" do
51
63
  with_files do
52
- dependencies = Rubydeps.dot_for do
64
+ ::Rubydeps.analyze do
53
65
  class IHaveAClassLevelDependency
54
66
  Son.class_method
55
67
  end
@@ -60,13 +72,13 @@ describe "Rubydeps" do
60
72
  end
61
73
 
62
74
  it "should be idempotent" do
63
- Rubydeps.hash_for do
75
+ ::Rubydeps.dependency_hash_for do
64
76
  class IHaveAClassLevelDependency
65
77
  Son.class_method
66
78
  end
67
79
  end
68
80
 
69
- dependencies = Rubydeps.hash_for do
81
+ dependencies, _ = ::Rubydeps.dependency_hash_for do
70
82
  class IHaveAClassLevelDependency
71
83
  Son.class_method
72
84
  end
@@ -75,20 +87,34 @@ describe "Rubydeps" do
75
87
  dependencies.should == {"Parent"=>["Son"]}
76
88
  end
77
89
 
90
+ it "should show the dependency from an object singleton method" do
91
+ dependencies, _ = ::Rubydeps.dependency_hash_for do
92
+ s = Son.new
93
+ def s.attached_method
94
+ Grandparent.class_method
95
+ end
96
+ s.attached_method
97
+ end
98
+
99
+ dependencies.keys.should == ["Grandparent", "GrandparentModule"]
100
+ dependencies["Grandparent"].should == ["Son"]
101
+ dependencies["GrandparentModule"].should == ["Grandparent"]
102
+ end
103
+
78
104
  it "should show the dependencies between the classes inside the block" do
79
- dependencies = Rubydeps.hash_for do
80
- Son.class_method
105
+ dependencies, _ = ::Rubydeps.dependency_hash_for do
81
106
  Son.new.instance_method
82
107
  end
83
108
 
84
- dependencies.keys.should =~ ["Parent", "Grandparent"]
109
+ dependencies.keys.should =~ ["Parent", "Grandparent", "GrandparentModule"]
85
110
  dependencies["Parent"].should == ["Son"]
86
111
  dependencies["Grandparent"].should =~ ["Son", "Parent"]
112
+ dependencies["GrandparentModule"].should == ["Grandparent"]
87
113
  end
88
114
 
89
115
  sample_dir_structure = {'path1/class_a.rb' => <<-CLASSA,
90
- require 'path1/class_b'
91
- require 'path2/class_c'
116
+ require './path1/class_b'
117
+ require './path2/class_c'
92
118
  class A
93
119
  def depend_on_b_and_c
94
120
  B.new.b
@@ -101,9 +127,9 @@ describe "Rubydeps" do
101
127
 
102
128
  it "should not filter classes when no filter is specified" do
103
129
  with_files(sample_dir_structure) do
104
- load 'path1/class_a.rb'
130
+ load './path1/class_a.rb'
105
131
 
106
- dependencies = Rubydeps.hash_for do
132
+ dependencies, _ = ::Rubydeps.dependency_hash_for do
107
133
  A.new.depend_on_b_and_c
108
134
  end
109
135
 
@@ -113,9 +139,9 @@ describe "Rubydeps" do
113
139
 
114
140
  it "should filter classes when a path filter is specified" do
115
141
  with_files(sample_dir_structure) do
116
- load 'path1/class_a.rb'
142
+ load './path1/class_a.rb'
117
143
 
118
- dependencies = Rubydeps.hash_for(:path_filter => /path1/) do
144
+ dependencies, _ = ::Rubydeps.dependency_hash_for(:path_filter => /path1/) do
119
145
  A.new.depend_on_b_and_c
120
146
  end
121
147
 
@@ -125,13 +151,34 @@ describe "Rubydeps" do
125
151
 
126
152
  it "should filter classes when a class name filter is specified" do
127
153
  with_files(sample_dir_structure) do
128
- load 'path1/class_a.rb'
154
+ load './path1/class_a.rb'
129
155
 
130
- dependencies = Rubydeps.hash_for(:class_name_filter => /C|A/) do
156
+ dependencies, _ = ::Rubydeps.dependency_hash_for(:class_name_filter => /C|A/) do
131
157
  A.new.depend_on_b_and_c
132
158
  end
133
159
 
134
160
  dependencies.should == {"C"=>["A"]}
135
161
  end
136
162
  end
163
+
164
+ it "should be capable of dumping the whole dependency data into a file for later filtering" do
165
+ with_files(sample_dir_structure) do
166
+ load './path1/class_a.rb'
167
+
168
+ ::Rubydeps.analyze(:to_file => 'dependencies.file') do
169
+ A.new.depend_on_b_and_c
170
+ end
171
+
172
+ dependencies, _ = ::Rubydeps.dependency_hash_for(:from_file => 'dependencies.file', :class_name_filter => /C|A/)
173
+ dependencies.should == {"C"=>["A"]}
174
+ end
175
+ end
176
+
177
+ it "should create correct dependencies for 2 instance methods called in a row" do
178
+ dependencies, _ = ::Rubydeps.dependency_hash_for do
179
+ Son.new.instance_method_calling_another_instance_method(Parent.new)
180
+ end
181
+
182
+ dependencies.should == {"Parent"=>["Son"]}
183
+ end
137
184
  end
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --color
data/spec/spec_helper.rb CHANGED
@@ -2,5 +2,5 @@ require 'rubydeps'
2
2
  require 'rspec'
3
3
  require 'rspec/autorun'
4
4
 
5
- Rspec.configure do |config|
5
+ RSpec.configure do |config|
6
6
  end
metadata CHANGED
@@ -1,13 +1,8 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubydeps
3
3
  version: !ruby/object:Gem::Version
4
- hash: 23
5
- prerelease:
6
- segments:
7
- - 0
8
- - 2
9
- - 0
10
- version: 0.2.0
4
+ prerelease: 6
5
+ version: 0.9.0.pre
11
6
  platform: ruby
12
7
  authors:
13
8
  - Daniel Cadenas
@@ -15,113 +10,108 @@ autorequire:
15
10
  bindir: bin
16
11
  cert_chain: []
17
12
 
18
- date: 2010-09-28 00:00:00 -03:00
19
- default_executable:
13
+ date: 2012-03-08 00:00:00 Z
20
14
  dependencies:
21
15
  - !ruby/object:Gem::Dependency
22
- name: rspec
16
+ name: rake-compiler
23
17
  prerelease: false
24
18
  requirement: &id001 !ruby/object:Gem::Requirement
25
19
  none: false
26
20
  requirements:
27
21
  - - ~>
28
22
  - !ruby/object:Gem::Version
29
- hash: 25
30
- segments:
31
- - 2
32
- - 5
33
- - 1
34
- version: 2.5.1
23
+ version: 0.8.0
35
24
  type: :development
36
25
  version_requirements: *id001
37
26
  - !ruby/object:Gem::Dependency
38
- name: file_test_helper
27
+ name: rspec
39
28
  prerelease: false
40
29
  requirement: &id002 !ruby/object:Gem::Requirement
41
30
  none: false
42
31
  requirements:
43
32
  - - ~>
44
33
  - !ruby/object:Gem::Version
45
- hash: 23
46
- segments:
47
- - 1
48
- - 0
49
- - 0
50
- version: 1.0.0
34
+ version: 2.8.0
51
35
  type: :development
52
36
  version_requirements: *id002
53
37
  - !ruby/object:Gem::Dependency
54
- name: ruby-graphviz
38
+ name: file_test_helper
55
39
  prerelease: false
56
40
  requirement: &id003 !ruby/object:Gem::Requirement
57
41
  none: false
58
42
  requirements:
59
43
  - - ~>
60
44
  - !ruby/object:Gem::Version
61
- hash: 25
62
- segments:
63
- - 0
64
- - 9
65
- - 17
66
- version: 0.9.17
67
- type: :runtime
45
+ version: 1.0.0
46
+ type: :development
68
47
  version_requirements: *id003
69
48
  - !ruby/object:Gem::Dependency
70
- name: rcov
49
+ name: ruby_core_source
71
50
  prerelease: false
72
51
  requirement: &id004 !ruby/object:Gem::Requirement
73
52
  none: false
74
53
  requirements:
75
54
  - - ~>
76
55
  - !ruby/object:Gem::Version
77
- hash: 43
78
- segments:
79
- - 0
80
- - 9
81
- - 8
82
- version: 0.9.8
56
+ version: 0.1.5
83
57
  type: :runtime
84
58
  version_requirements: *id004
85
59
  - !ruby/object:Gem::Dependency
86
- name: thor
60
+ name: ruby-graphviz
87
61
  prerelease: false
88
62
  requirement: &id005 !ruby/object:Gem::Requirement
89
63
  none: false
90
64
  requirements:
91
65
  - - ~>
92
66
  - !ruby/object:Gem::Version
93
- hash: 35
94
- segments:
95
- - 0
96
- - 14
97
- - 2
98
- version: 0.14.2
67
+ version: 1.0.5
99
68
  type: :runtime
100
69
  version_requirements: *id005
101
- description: Graphs ruby dependencies
70
+ - !ruby/object:Gem::Dependency
71
+ name: thor
72
+ prerelease: false
73
+ requirement: &id006 !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ~>
77
+ - !ruby/object:Gem::Version
78
+ version: 0.14.2
79
+ type: :runtime
80
+ version_requirements: *id006
81
+ description: A tool to create class dependency graphs from test suites
102
82
  email: dcadenas@gmail.com
103
83
  executables:
104
84
  - rubydeps
105
- extensions: []
85
+ extensions:
86
+ - ext/call_site_analyzer/extconf.rb
87
+ extra_rdoc_files: []
106
88
 
107
- extra_rdoc_files:
108
- - LICENSE
109
89
  files:
110
90
  - .document
111
91
  - .gitignore
92
+ - .travis.yml
93
+ - Gemfile
94
+ - Gemfile.lock
112
95
  - LICENSE
113
96
  - README.md
97
+ - Rakefile
98
+ - VERSION
114
99
  - bin/rubydeps
100
+ - ext/call_site_analyzer/call_site_analyzer.c
101
+ - ext/call_site_analyzer/extconf.rb
102
+ - lib/call_site_analyzer.bundle
115
103
  - lib/rubydeps.rb
104
+ - rake-deps.png
105
+ - rubydeps.gemspec
116
106
  - spec/rubydeps_spec.rb
107
+ - spec/spec.opts
117
108
  - spec/spec_helper.rb
118
- has_rdoc: true
119
109
  homepage: http://github.com/dcadenas/rubydeps
120
110
  licenses: []
121
111
 
122
112
  post_install_message:
123
- rdoc_options:
124
- - --charset=UTF-8
113
+ rdoc_options: []
114
+
125
115
  require_paths:
126
116
  - lib
127
117
  required_ruby_version: !ruby/object:Gem::Requirement
@@ -129,26 +119,19 @@ required_ruby_version: !ruby/object:Gem::Requirement
129
119
  requirements:
130
120
  - - ">="
131
121
  - !ruby/object:Gem::Version
132
- hash: 3
133
- segments:
134
- - 0
135
122
  version: "0"
136
123
  required_rubygems_version: !ruby/object:Gem::Requirement
137
124
  none: false
138
125
  requirements:
139
- - - ">="
126
+ - - ">"
140
127
  - !ruby/object:Gem::Version
141
- hash: 3
142
- segments:
143
- - 0
144
- version: "0"
128
+ version: 1.3.1
145
129
  requirements: []
146
130
 
147
131
  rubyforge_project:
148
- rubygems_version: 1.6.2
132
+ rubygems_version: 1.8.16
149
133
  signing_key:
150
134
  specification_version: 3
151
- summary: Graphs ruby depencencies
152
- test_files:
153
- - spec/rubydeps_spec.rb
154
- - spec/spec_helper.rb
135
+ summary: A tool to create class dependency graphs from test suites
136
+ test_files: []
137
+