Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

merge jscruggs master branch

  • Loading branch information...
commit 7ccc20442ddeb9d0c2e103232f5b3039ac63ba91 1 parent 94b32f1
@flyerhzm flyerhzm authored
Showing with 3,972 additions and 3,028 deletions.
  1. +2 −0  .gitignore
  2. +64 −3 HISTORY
  3. +1 −1  MIT-LICENSE
  4. +29 −0 README
  5. +0 −3  README.textile
  6. +5 −5 Rakefile
  7. +3 −6 TODO
  8. +12 −0 config/roodi_config.yml
  9. BIN  home_page/hotspot.gif
  10. +101 −61 home_page/index.html
  11. BIN  home_page/reek.gif
  12. BIN  home_page/roodi.gif
  13. BIN  home_page/stats.gif
  14. +36 −14 lib/base/base_template.rb
  15. +52 −0 lib/base/churn_analyzer.rb
  16. +97 −0 lib/base/code_issue.rb
  17. +41 −27 lib/base/configuration.rb
  18. +50 −0 lib/base/flay_analyzer.rb
  19. +43 −0 lib/base/flog_analyzer.rb
  20. +22 −27 lib/base/generator.rb
  21. +15 −10 lib/base/graph.rb
  22. +66 −0 lib/base/line_numbers.rb
  23. +85 −0 lib/base/location.rb
  24. +404 −0 lib/base/metric_analyzer.rb
  25. +34 −0 lib/base/ranking.rb
  26. +43 −0 lib/base/rcov_analyzer.rb
  27. +116 −0 lib/base/reek_analyzer.rb
  28. +9 −9 lib/base/report.rb
  29. +37 −0 lib/base/roodi_analyzer.rb
  30. +48 −0 lib/base/saikuro_analyzer.rb
  31. +29 −0 lib/base/scoring_strategies.rb
  32. +37 −0 lib/base/stats_analyzer.rb
  33. +102 −0 lib/base/table.rb
  34. +11 −73 lib/generators/churn.rb
  35. +6 −9 lib/generators/flay.rb
  36. +59 −128 lib/generators/flog.rb
  37. +52 −0 lib/generators/hotspots.rb
  38. +0 −6 lib/generators/rails_best_practices.rb
  39. +70 −31 lib/generators/rcov.rb
  40. +3 −7 lib/generators/reek.rb
  41. +2 −7 lib/generators/roodi.rb
  42. +78 −62 lib/generators/saikuro.rb
  43. +30 −14 lib/generators/stats.rb
  44. +17 −2 lib/graphs/engines/bluff.rb
  45. +49 −11 lib/graphs/engines/gchart.rb
  46. +6 −8 lib/graphs/flay_grapher.rb
  47. +28 −11 lib/graphs/flog_grapher.rb
  48. +1 −1  lib/graphs/grapher.rb
  49. +7 −8 lib/graphs/rails_best_practices_grapher.rb
  50. +6 −8 lib/graphs/rcov_grapher.rb
  51. +15 −17 lib/graphs/reek_grapher.rb
  52. +6 −8 lib/graphs/roodi_grapher.rb
  53. +20 −0 lib/graphs/stats_grapher.rb
  54. +11 −3 lib/metric_fu.rb
  55. +2 −2 lib/templates/awesome/awesome_template.rb
  56. +40 −1 lib/templates/awesome/churn.html.erb
  57. +16 −0 lib/templates/awesome/css/default.css
  58. +0 −1  lib/templates/awesome/css/integrity.css
  59. +3 −2 lib/templates/awesome/flay.html.erb
  60. +19 −17 lib/templates/awesome/flog.html.erb
  61. +54 −0 lib/templates/awesome/hotspots.html.erb
  62. +5 −2 lib/templates/awesome/index.html.erb
  63. +1 −1  lib/templates/awesome/rails_best_practices.html.erb
  64. +1 −1  lib/templates/awesome/rcov.html.erb
  65. +1 −1  lib/templates/awesome/roodi.html.erb
  66. +3 −3 lib/templates/awesome/saikuro.html.erb
  67. +11 −1 lib/templates/awesome/stats.html.erb
  68. +1 −1  lib/templates/javascripts/bluff-min.js
  69. +35 −19 lib/templates/javascripts/excanvas.js
  70. +1 −1  lib/templates/javascripts/js-class.js
  71. +2 −2 lib/templates/standard/churn.html.erb
  72. +4 −4 lib/templates/standard/default.css
  73. +4 −4 lib/templates/standard/flay.html.erb
  74. +28 −24 lib/templates/standard/flog.html.erb
  75. +54 −0 lib/templates/standard/hotspots.html.erb
  76. +2 −2 lib/templates/standard/index.html.erb
  77. +2 −2 lib/templates/standard/rails_best_practices.html.erb
  78. +2 −2 lib/templates/standard/rcov.html.erb
  79. +1 −1  lib/templates/standard/reek.html.erb
  80. +2 −2 lib/templates/standard/roodi.html.erb
  81. +4 −4 lib/templates/standard/saikuro.html.erb
  82. +2 −2 lib/templates/standard/stats.html.erb
  83. +21 −25 metric_fu.gemspec
  84. +1 −1  spec/base/base_template_spec.rb
  85. +47 −40 spec/base/configuration_spec.rb
  86. +10 −31 spec/base/generator_spec.rb
  87. +41 −4 spec/base/graph_spec.rb
  88. +62 −0 spec/base/line_numbers_spec.rb
  89. +9 −9 spec/base/report_spec.rb
  90. +16 −127 spec/generators/churn_spec.rb
  91. +41 −40 spec/generators/flay_spec.rb
  92. +58 −226 spec/generators/flog_spec.rb
  93. +52 −0 spec/generators/rails_best_practices_spec.rb
  94. +180 −0 spec/generators/rcov_spec.rb
  95. +21 −13 spec/generators/reek_spec.rb
  96. +24 −0 spec/generators/roodi_spec.rb
  97. +7 −7 spec/generators/saikuro_spec.rb
  98. +6 −6 spec/generators/stats_spec.rb
  99. +8 −4 spec/graphs/engines/bluff_spec.rb
  100. +151 −10 spec/graphs/engines/gchart_spec.rb
  101. +35 −16 spec/graphs/flay_grapher_spec.rb
  102. +76 −13 spec/graphs/flog_grapher_spec.rb
  103. +61 −0 spec/graphs/rails_best_practices_grapher_spec.rb
  104. +35 −16 spec/graphs/rcov_grapher_spec.rb
  105. +44 −25 spec/graphs/reek_grapher_spec.rb
  106. +35 −16 spec/graphs/roodi_grapher_spec.rb
  107. +68 −0 spec/graphs/stats_grapher_spec.rb
  108. +33 −0 spec/resources/line_numbers/foo.rb
  109. +11 −0 spec/resources/line_numbers/module.rb
  110. +15 −0 spec/resources/line_numbers/module_surrounds_class.rb
  111. +11 −0 spec/resources/line_numbers/two_classes.rb
  112. +2 −2 spec/resources/saikuro/index_cyclo.html
  113. +424 −346 spec/resources/yml/20090630.yml
  114. +1 −0  spec/resources/yml/metric_missing.yml
  115. +2 −4 spec/spec.opts
  116. +4 −4 tasks/metric_fu.rake
  117. BIN  vendor/_fonts/monaco.ttf
  118. +0 −142 vendor/saikuro/SAIKURO_README
  119. +0 −1,219 vendor/saikuro/saikuro.rb
View
2  .gitignore
@@ -2,3 +2,5 @@
previous_failures.txt
tmp/*
*metric_fu-*.gem
+*.html_complexity.*
+scratch_directory/
View
67 HISTORY
@@ -1,3 +1,64 @@
+=== MetricFu 2.0.2 / ???
+
+* Flog gemspec version was >= 2.2.0, which was too early and didn't work. Changed too >= 2.3.0 - Chris Griego
+
+=== MetricFu 2.0.1 / 2010-11-13
+
+* Delete trailing whitespaces - Delwyn de Villiers
+* Stop Ubuntu choking on invalid multibyte char (US-ASCII) - Delwyn de Villiers
+* Fix invalid next in lib/base/metric_analyzer.rb - Delwyn de Villiers
+* Don't load Saikuro for Ruby 1.9.2 - Delwyn de Villiers
+* Fixed a bug reported by Andrew Davis on the mailing list where configuring the data directory causes dates to be 0/0 - Joshua Cronemeyer
+
+=== MetricFu 2.0.0 / 2010-11-10
+
+* Hotspots - Dan Mayer, Ben Brinckerhoff, Jake Scruggs
+* Rcov integration with Hotspots - Jake Scruggs, Tony Castiglione, Rob Meyer
+
+=== MetricFu 1.5.1 / 2010-7-28
+
+* Patch that allows graphers to skip dates that didn't generate metrics for that graph (GitHub Issue #20). - Chris Griego
+* Fixed bug where if you try and use the gchart grapher with the rails_best_practices metric, it blows up (GitHub Issue #23). - Chris Griego
+* Fixed 'If coverage is 0% metric_fu will explode' bug (GitHub Issue #6). - Stew Welbourne
+
+=== MetricFu 1.5.0 / 2010-7-27
+
+* Fixed bug where Flay results were not being reported. Had to remove the ability to remove selected files from flay processing (undocumented feature that may go away soon if it keeps causing problems).
+* Rewrote Flog parsing/processing to use Flog programmatically. Note: the yaml output for Flog has changed significantly - Pages have now become MethodContainers. This probably doesn't matter to you if you are not consuming the metric_fu yaml output.
+* Added support for using config files in Reek and Roodi (roodi support was already there but undocumented).
+* Removed verify_dependencies! as it caused too much confusion to justify the limited set of problems it solved. In the post Bundler world it just didn't seem necessary to limit metric_fu dependencies.
+* Deal with Rails 3 activesupport vs active_support problems. - jinzhu
+
+=== MetricFu 1.4.0 / 2010-06-19
+
+* Added support for rails_best_practices gem - Richard Huang
+* Added rails stats graphing -- Josh Cronemeyer
+* Parameterize the filetypes for flay. By default flay supports haml as well as rb and has a plugin ability for other filetypes. - bfabry
+* Support for Flog 2.4.0 line numbers - Dan Mayer
+* Saikuro multi input directory patch - Spencer Dillard and Dan Mayer
+* Can now parse rcov analysis file coming from multiple sources with an rcov :external option in the config. - Tarsoly András
+* Fixed open file handles problem in the Saikuro analyzer - aselder, erebor
+* Fix some problems with the google charts - Chris Griego
+* Stop showing the googlecharts warning if you are not using google charts.
+
+=== MetricFu 1.3.0 / 2010-01-26
+
+* Flay can be configured to ignore scores below a threshold (by default it ignores scores less than 100)
+* When running Rcov you can configure the RAILS_ENV (defaults to 'test') so running metric_fu doesn't interfere with other environments
+* Changed devver-construct (a gem hosted by GitHub) development dependency to test-construct dependency (on Gemcutter) - Dan Mayer
+* Upgrade Bluff to 0.3.6 and added tooltips to graphs - Édouard Brière
+* Removed Saikuro from vendor and added it as a gem dependency - Édouard Brière
+* Churn has moved outside metric_fu and is now a gem and a dependency - Dan Mayer
+* Fix 'activesupport' deprecation (it should be 'active_support') - Bryan Helmkamp
+* Declared development dependencies
+* Cleaned and sped up specs
+
+=== MetricFu 1.2.0 / 2010-01-09
+
+* ftools isn't supported by 1.9 so moved to fileutils.
+* Overhauled the graphing to use Gruff or Google Charts so we no longer depend on ImageMagick/rmagick -- thanks to Carl Youngblood.
+* Stopped relying on Github gems as they will be going away.
+
=== MetricFu 1.1.6 / 2009-12-14
* Now compatible with Reek 1.2x thanks to Kevin Rutherford
@@ -110,7 +171,7 @@
=== MetricFu 0.7.0 / 2008-09-11
-* Merged in Sean Soper's changes to metric_fu.
+* Merged in Sean Soper's changes to metric_fu.
* Metric_fu is now a gem.
* Flogging now uses a MD5 hash to figure out if it should re-flog a file (if it's changed)
* Flogging also has a cool new output screen(s)
@@ -145,7 +206,7 @@
=== Metricks 0.1.0 / 2008-06-10
* Initial integration of metric_fu and my enhancements to flog
- * Metrics are generated but are all over the place
+ * Metrics are generated but are all over the place
=== MetricFu 0.6.0 / 2008-05-11
@@ -153,7 +214,7 @@
=== MetricFu 0.5.1 / 2008-04-25
-* Fixed bug with Saikuro report generation
+* Fixed bug with Saikuro report generation - thanks Toby Tripp
=== MetricFu 0.5.0 / 2008-04-25
View
2  MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2008,2009 Jake Scruggs
+Copyright (c) 2008,2009,2010 Jake Scruggs
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
View
29 README
@@ -0,0 +1,29 @@
+See http://metric-fu.rubyforge.org/ for documentation, or the HISTORY file for a change log.
+
+How to contribute:
+
+1. Fork metric_fu on github.
+2. 'gem install metric_fu' #to get gem dependencies
+3. Install the gems RSpec 1.3.0, test-construct, and googlecharts
+4. Run the tests ('rake')
+5. Run metric_fu on itself ('rake metrics:all')
+6. Make the changes you want and back them up with tests.
+7. Make sure two important rake tests still run ('rake' and 'rake metrics:all')
+8. Commit and send me a pull request with details as to what has been changed.
+
+Extra Credit:
+1. Make sure your changes work in 1.8.7, Ruby Enterprise Edition, and 1.9.1 (Hint use 'rvm' to help install multiple rubies)
+2. Post to the Google group explaining what you did and why you did it (I don't merge things in immediately so others might want to use what you've done).
+3. Update the documentation (web page inside the 'home_page' folder)
+4. Update the History and give yourself credit.
+
+
+The more of the above steps you do the easier it will be for me to merge in which will greatly increase you chances of getting your changes accepted.
+If you want to do something really radical (which will touch over %30 of all the files) you might what to ask the Google group first to see if anyone is interested in your change before you spend a lot of time on it.
+
+Resources:
+Homepage: http://metric-fu.rubyforge.org/
+Github: http://github.com/jscruggs/metric_fu
+Google Group: http://groups.google.com/group/metric_fu
+Issue Tracker: http://github.com/jscruggs/metric_fu/issues
+My Blog: http://jakescruggs.blogspot.com/
View
3  README.textile
@@ -1,3 +0,0 @@
-h1. flyerhzm-metric_fu
-
-rails_best_practices gem has been merged into official metric_fu gem, please upgrade to metric_fu 1.4.0 to enjoy the rails_best_practices. "http://github.com/jscruggs/metric_fu":http://github.com/jscruggs/metric_fu
View
10 Rakefile
@@ -1,3 +1,4 @@
+$LOAD_PATH << '.'
require 'rake'
require 'rake/rdoctask'
require 'spec/rake/spectask'
@@ -6,13 +7,12 @@ require 'lib/metric_fu'
desc "Run all specs in spec directory"
Spec::Rake::SpecTask.new(:spec) do |t|
t.spec_files = FileList['spec/**/*_spec.rb']
- t.rcov = true
- t.rcov_opts = ['--exclude', 'spec,config,Library,usr/lib/ruby']
- t.rcov_dir = File.join(File.dirname(__FILE__), "tmp")
end
MetricFu::Configuration.run do |config|
- config.template_class = AwesomeTemplate
+ config.roodi = config.roodi.merge(:roodi_config => 'config/roodi_config.yml')
+ config.churn = { :start_date => "1 year ago", :minimum_churn_count => 10}
+ config.hotspots = { :start_date => "1 year ago", :minimum_churn_count => 10}
end
-task :default => [:"metrics:all"]
+task :default => :spec
View
9 TODO
@@ -1,9 +1,6 @@
== TODO list
* Color code flog results with scale from: http://jakescruggs.blogspot.com/2008/08/whats-good-flog-score.html
-* Integrate Flog, Saikuro, and Coverage into one report so you can see methods that have high complexity and low coverage (this is a big one)
-* Update flog specs so that they actually run flog
-* Add flay specs that run flay
-* Convert readme to markdown and rename to README.mkdn so github will render it
-* Saikuro.rb falls over if tries to parse an empty file. Fair enough. We shouldn't feed it empty files
-* Make running metric_fu on metric_fu less embarrassing
+* Make running metric_fu on metric_fu less embarrassing
+* Load all gems at config time so you fail fast if one is missing
+
View
12 config/roodi_config.yml
@@ -0,0 +1,12 @@
+CaseMissingElseCheck: { }
+ClassLineCountCheck: { line_count: 300 }
+ClassNameCheck: { pattern: !ruby/regexp /^[A-Z][a-zA-Z0-9]*$/ }
+CyclomaticComplexityBlockCheck: { complexity: 4 }
+CyclomaticComplexityMethodCheck: { complexity: 8 }
+EmptyRescueBodyCheck: { }
+ForLoopCheck: { }
+MethodLineCountCheck: { line_count: 20 }
+MethodNameCheck: { pattern: !ruby/regexp /^[_a-z<>=\[\]|+-\/\*`]+[_a-z0-9_<>=~@\[\]]*[=!\?]?$/ }
+ModuleLineCountCheck: { line_count: 300 }
+ModuleNameCheck: { pattern: !ruby/regexp /^[A-Z][a-zA-Z0-9]*$/ }
+ParameterNumberCheck: { parameter_count: 5 }
View
BIN  home_page/hotspot.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
162 home_page/index.html
@@ -35,6 +35,10 @@
SCM
</a>
</li>
+ <li><a href="http://github.com/jscruggs/metric_fu/issues">
+ Issue Tracker
+ </a>
+ </li>
<li><a href="http://jakescruggs.blogspot.com/">
Jake Scruggs' Blog (lead developer)
</a>
@@ -46,7 +50,7 @@
<!-- end #sidebar -->
</div>
<div id="left">
- <h1>About metric_fu 1.1.6</h1>
+ <h1>About metric_fu 2.0.1</h1>
<div>
<p><br/>
Metric_fu is a set of rake tasks that make it easy to generate metrics reports. It uses
@@ -54,8 +58,10 @@
<a href="http://ruby.sadi.st/Flog.html">Flog</a>,
<a href="http://ruby.sadi.st/Flay.html">Flay</a>,
<a href="http://eigenclass.org/hiki.rb?rcov">Rcov</a>,
- <a href="http://github.com/kevinrutherford/reek/tree/master">Reek</a>,
- <a href="http://github.com/martinjandrews/roodi/tree/master">Roodi</a>,
+ <a href="http://github.com/kevinrutherford/reek">Reek</a>,
+ <a href="http://github.com/martinjandrews/roodi">Roodi</a>,
+ <a href="http://github.com/danmayer/churn">Churn</a>,
+ <a href="http://rubygems.org/gems/rails_best_practices">RailsBestPractices</a>,
<a href="http://subversion.tigris.org/">Subversion</a>,
<a href="http://git.or.cz/">Git</a>, and
<a href="http://www.rubyonrails.org/">Rails</a>
@@ -65,46 +71,31 @@
</p>
<br/>
<p>
- New in metric_fu 1.1.0 is integration of the 'Awesome' template and graphs -- which graph your metric performance over time! Big thanks go to <a href="http://litanyagainstfear.com/">Nick Quaranto</a> and <a href="http://www.nanalegumene.net/">Edouard Brière</a> for their work on this feature. If you're used to the non-awesome reports then this will be quite a treat. Graphing requires the gems 'gruff' and 'rmagick' (and rmagick requires ImageMagick). If that's too much installation to bear, then see configuration section below for how to turn off graphing of the metrics.
+ In 2.0.0 the big new feature is Hotspots. The Hotspots report combines Flog, Flay, Rcov, Reek, Roodi, and Churn numbers into one report so you see parts of your code that have multiple problems like so:
</p>
-<br/>
+<img src="hotspot.gif" width="359">
<p>
- New in metric_fu 1.0.0 is yaml serialization of the metrics. Running metric_fu now generates, in addition to html reports, a report.yml file. You can consume this file to mash up metrics, track them, or whatever you desire. Many thanks go to <a href="http://github.com/gmcinnes">Grant McInnes</a> for his work on this feature.
+That is one terrible method.<br/>
+Big thanks to Dan Mayer and Ben Brinckerhoff for the Hotspots code and for helping me integrate it with RCov.
</p>
-<h2>Installation:</h2>
-<pre>sudo gem install metric_fu</pre>
-<br/>
-<p>Then in your Rakefile:</p>
-<br/>
-<pre>require 'metric_fu'</pre>
<br/>
+
<p>
-If you like to vendor gems, you can unpack metric_fu into vendor/gems and require it like so:
+ In 1.5.0 I re-wrote the Flog parsing engine to use Flog programmatically instead of parsing the command line output. This will hopefully stop minor changes in Flog's output from breaking metric_fu. Check the <a href="http://github.com/jscruggs/metric_fu/blob/master/HISTORY">HISTORY</a> file for complete details.
</p>
<br/>
-<pre>require(File.join(RAILS_ROOT, 'vendor', 'gems', 'metric_fu-1.1.6', 'lib', 'metric_fu'))</pre>
-<br/>
-<p>Then you don't have to install it on every box you run it on.</p>
-<br/>
-<p>Later versions of Rails like to manage your gems for you, so you can put this in your test.rb file:</p>
-<br/>
-<pre>config.gem 'metric_fu', :version => '1.1.6', :lib => 'metric_fu'</pre>
-<br/>
-<p>And then issue this command:</p>
+<p>
+ New in metric_fu 1.4.0 is integration with the rails_best_practices gem (Richard Huang), Rails stats now have graphing (Josh Cronemeyer), parameterized filetypes for flay so it can look at other file types (bfabry), along with many bug fixes.
+</p>
<br/>
-<pre>
- $ rake gems:unpack RAILS_ENV=test
-</pre>
+
+<h2>Installation:</h2>
+<pre>gem install metric_fu</pre>
<br/>
-<p>That way Rails won't yell at you every time you run a Rake task:</p>
+<p>Then in your Rakefile:</p>
<br/>
-<pre>
- config.gem: Unpacked gem metric_fu-1.1.6 in vendor/gems has no specification file. Run 'rake gems:refresh_specs' to fix this.
-</pre>
+<pre>require 'metric_fu'</pre>
<br/>
-<p>
- I hate being yelled at.
-</p>
<h2>Output:</h2>
<table>
@@ -141,45 +132,74 @@
<br/>
<h2>Configuration:</h2>
+ <p>The definitive source for configuration is, of course, the source: <a href="http://github.com/jscruggs/metric_fu/blob/master/lib/base/configuration.rb#L99">lib/base/configuration.rb</a>
<p>Metric_fu can be customized to your liking by altering the defaults in your Rakefile:</p>
<pre>
MetricFu::Configuration.run do |config|
#define which metrics you want to use
- config.metrics = [:churn, :saikuro, :stats, :flog, :flay, :reek, :roodi, :rcov]
- config.graphs = [:flog, :flay, :reek, :roodi, :rcov]
- config.flay = { :dirs_to_flay => ['app', 'lib'] }
- config.flog = { :dirs_to_flog => ['app', 'lib'] }
- config.reek = { :dirs_to_reek => ['app', 'lib'] }
- config.roodi = { :dirs_to_roodi => ['app', 'lib'] }
- config.saikuro = { :output_directory => 'scratch_directory/saikuro',
- :input_directory => ['app', 'lib'],
- :cyclo => "",
- :filter_cyclo => "0",
- :warn_cyclo => "5",
- :error_cyclo => "7",
- :formater => "text"} #this needs to be set to "text"
- config.churn = { :start_date => "1 year ago", :minimum_churn_count => 10}
- config.rcov = { :test_files => ['test/**/*_test.rb',
- 'spec/**/*_spec.rb'],
- :rcov_opts => ["--sort coverage",
- "--no-html",
- "--text-coverage",
- "--no-color",
- "--profile",
- "--rails",
- "--exclude /gems/,/Library/,spec"]}
+ config.metrics = [:churn, :saikuro, :stats, :flog, :flay]
+ config.graphs = [:flog, :flay, :stats]
+ # ...
end
</pre>
Note: if you don't want one of the metrics to run (like rcov), make sure you don't try to graph it (which will explode). Set config.graphs to exactly the graphs you want. Set config.graphs to an empty array (config.graphs = []) if you want no graphing at all.
</p>
<p><br/>
+ <h2>Graphing</h2>
+ MetricFu uses Bluff for graphing (a Javascript library) by default. If you'd rather use Google Charts you can set 'config.graph_engine = :gchart' in the config. You'll have to install the googlecharts gem, of course.
+</p>
+
+<p><br/>
+ <h2>Notes on Rails 3</h2>
+ If you use RSpec, you'll need to add this to one of your rake files:
+ <pre>
+ require 'metric_fu'
+ MetricFu::Configuration.run do |config|
+ config.rcov[:test_files] = ['spec/**/*_spec.rb']
+ config.rcov[:rcov_opts] << "-Ispec" # Needed to find spec_helper
+ end
+ </pre>
+ If you're using default Rails testing, then you can do this:
+ <pre>
+ require 'metric_fu'
+ MetricFu::Configuration.run do |config|
+ config.rcov[:rcov_opts] << "-Itest" # Needed to find test_helper
+ end
+ </pre>
+<br/>
+
+<p><br/>
<h2>Notes on metrics:coverage</h2>
- When creating a coverage report, metric_fu runs all the tests in the test folder and specs in spec folder using Rcov.
+ When creating a coverage report, metric_fu runs all the tests in the test folder and specs in spec folder using Rcov. You can configure what files it should run and the RAILS_ENV (by setting 'environment') it runs under. Default config for rcov:
+ <pre>
+ config.rcov = { :environment => 'test',
+ :test_files => ['test/**/*_test.rb',
+ 'spec/**/*_spec.rb'],
+ :rcov_opts => ["--sort coverage",
+ "--no-html",
+ "--text-coverage",
+ "--no-color",
+ "--profile",
+ "--rails",
+ "--exclude /gems/,/Library/,/usr/,spec"],
+ :external => nil
+ }
+ </pre>
+ By far, Rcov is the most troublesome report in metric_fu. This is not because of Rcov itself, but because the Rcov report has to actually execute your tests and test suites can be very tricky things. MetricFu naively shells out to the command line, runs rcov on your tests/specs, and captures the output. This does not always work well. If you are having problems with metric_fu, try turning off Rcov (see the above Configuration section).
</p>
<p><br/>
<h2>Notes on metrics:saikuro</h2>
- Saikuro is bundled with metric_fu so you don't have to install it. Look at the SAIKURO_README (or the internet) for more documentation on Saikuro.
+ Saikuro measures cyclomatic complexity for Ruby. Default config for Saikuro:
+ <pre>
+ config.saikuro = { :output_directory => 'tmp/metirc_fu/saikuro',
+ :input_directory => ['app', 'lib'],
+ :cyclo => "",
+ :filter_cyclo => "0",
+ :warn_cyclo => "5",
+ :error_cyclo => "7",
+ :formater => "text"}
+ </pre>
<br/><br/>
</p>
<p><br/>
@@ -187,26 +207,44 @@
Flay analyzes ruby code for structural similarities.
You can configure which directories need to be flayed.
The defaults are 'lib' for non Rails projects and ['app', 'lib'] for Rails projects.
+ By default, metric_fu ignores scores under 100. You can configure the minimum_score. Default config for Flay:
+ <pre>
+ config.flay ={:dirs_to_flay => ['app', 'lib'],
+ :minimum_score => 100,
+ :filetypes => ['rb'] }
+ </pre>
</p>
<p><br/>
<h2>Notes on metrics:flog</h2>
- Note: Flog 2.1.2 has some issues with Ruby 1.8.6 -- try using Flog 2.1.0 (Flog 2.1.2 does, however, work fine with Ruby 1.9.1)
<br/>
- Flog is another way of measuring complexity (or tortured code as the Flog authors like to put it). You should check out the awesome, and a little scary, <a href="http://ruby.sadi.st/Flog.html">Flog website</a> for more info.
+ Flog is another way of measuring complexity (or tortured code as the Flog authors like to put it). You should check out the awesome, and a little scary, <a href="http://ruby.sadi.st/Flog.html">Flog website</a> for more info. Default config for Flog:
+ <pre>
+ config.flog = { :dirs_to_flog => ['app', 'lib'] }
+ </pre>
</p>
<p><br/>
<h2>Notes on metrics:reek</h2>
Reek detects common code smells in ruby code.
You can configure which directories need to be checked.
- The defaults are 'lib' for non Rails projects and ['app', 'lib'] for Rails projects.
+ The defaults are 'lib' for non Rails projects and ['app', 'lib'] for Rails projects. Default config for Reek:
+ <pre>
+ config.reek = { :dirs_to_reek => ['app', 'lib'] }
+ </pre>
</p>
<p><br/>
<h2>Notes on metrics:roodi</h2>
Roodi parses your Ruby code and warns you about design issues you have based on the checks that is has configured.
You can configure which directories need to be checked.
- The defaults are 'lib' for non Rails projects and ['app', 'lib'] for Rails projects.
+ The defaults are 'lib' for non Rails projects and ['app', 'lib'] for Rails projects. Default config for Roodi:
+ <pre>
+ config.roodi = { :dirs_to_roodi => ['app', 'lib'] }
+ </pre>
+</p>
+<p><br/>
+ <h2>Notes on metrics:rails_best_practices</h2>
+ Rails Best Practices checks your rails files for violations of... Well the name says it all. The gem was inspired by ihower's presentation <a href="http://www.slideshare.net/ihower/rails-best-practices">Rails Best Practices</a>
</p>
<p><br/>
<h2>Notes on metrics:stats</h2>
@@ -218,6 +256,8 @@
<br/>
<br/>
</p>
+
+
<br/>
<p>Metric_fu began its life as a plugin for Rails that generated code metrics reports. As of version 0.7.0, metric_fu is a gem (owing to the excellent work done by Sean Soper) and is hosted on GitHub at <a href="http://github.com/jscruggs/metric_fu">http://github.com/jscruggs/metric_fu</a>. </p>
<p><br/>
View
BIN  home_page/reek.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  home_page/roodi.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  home_page/stats.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
50 lib/base/base_template.rb
@@ -9,12 +9,12 @@ module MetricFu
# example.
class Template
attr_accessor :report
-
+
private
# Creates a new erb evaluated result from the passed in section.
#
# @param section String
- # The section name of
+ # The section name of
#
# @return String
# The erb evaluated string
@@ -35,10 +35,10 @@ def erbify(section)
def template_exists?(section)
File.exist?(template(section))
end
-
+
# Copies an instance variable mimicing the name of the section
- # we are trying to render, with a value equal to the passed in
- # constant. Allows the concrete template classes to refer to
+ # we are trying to render, with a value equal to the passed in
+ # constant. Allows the concrete template classes to refer to
# that instance variable from their ERB rendering
#
# @param section String
@@ -48,10 +48,10 @@ def template_exists?(section)
# The value to set as the value of the created instance
# variable
def create_instance_var(section, contents)
- instance_variable_set("@#{section}", contents)
+ instance_variable_set("@#{section}", contents)
end
- # Generates the filename of the template file to load and
+ # Generates the filename of the template file to load and
# evaluate. In this case, the path to the template directory +
# the section name + .html.erb
#
@@ -76,7 +76,7 @@ def output_filename(section)
section.to_s + ".html"
end
- # Returns the contents of a given css file in order to
+ # Returns the contents of a given css file in order to
# render it inline into a template.
#
# @param css String
@@ -85,14 +85,14 @@ def output_filename(section)
# @return String
# The contents of the css file
def inline_css(css)
- open(File.join(this_directory, css)) { |f| f.read }
+ open(File.join(this_directory, css)) { |f| f.read }
end
-
+
# Provides a link to open a file through the textmate protocol
# on Darwin, or otherwise, a simple file link.
#
# @param name String
- #
+ #
# @param line Integer
# The line number to link to, if textmate is available. Defaults
# to nil
@@ -102,7 +102,12 @@ def inline_css(css)
def link_to_filename(name, line = nil, link_content = nil)
"<a href='#{file_url(name, line)}'>#{link_content(name, line, link_content)}</a>"
end
-
+
+ def round_to_tenths(decimal)
+ decimal = 0.0 if decimal.to_s.eql?('NaN')
+ (decimal * 10).round / 10.0
+ end
+
def link_content(name, line=nil, link_content=nil) # :nodoc:
if link_content
link_content
@@ -112,12 +117,29 @@ def link_content(name, line=nil, link_content=nil) # :nodoc:
name
end
end
-
+
+ def display_location(location, stat)
+ file_path, class_name, method_name = location.file_path, location.class_name, location.method_name
+ str = ""
+ str += link_to_filename(file_path)
+ str += " : " if method_name || class_name
+ if(method_name)
+ str += "#{method_name}"
+ else
+ #TODO HOTSPOTS BUG ONLY exists on move over to metric_fu
+ if class_name.is_a?(String)
+ str+= "#{class_name}"
+ end
+ end
+ str
+ end
+
def file_url(name, line) # :nodoc:
+ return '' unless name
filename = File.expand_path(name.gsub(/^\//, ''))
if MetricFu.configuration.platform.include?('darwin')
"txmt://open/?url=file://#{filename}" << (line ? "&line=#{line}" : "")
- else
+ else
"file://#{filename}"
end
end
View
52 lib/base/churn_analyzer.rb
@@ -0,0 +1,52 @@
+class ChurnAnalyzer
+ include ScoringStrategies
+
+ COLUMNS = %w{times_changed}
+
+ def columns
+ COLUMNS
+ end
+
+ def name
+ :churn
+ end
+
+ def map(row)
+ ScoringStrategies.present(row)
+ end
+
+ def reduce(scores)
+ ScoringStrategies.sum(scores)
+ end
+
+ def score(metric_ranking, item)
+ flat_churn_score = 0.50
+ metric_ranking.scored?(item) ? flat_churn_score : 0
+ end
+
+ def generate_records(data, table)
+ return if data==nil
+ Array(data[:changes]).each do |change|
+ table << {
+ "metric" => :churn,
+ "times_changed" => change[:times_changed],
+ "file_path" => change[:file_path]
+ }
+ end
+ end
+
+ private
+
+ def self.update_changes(total, changed)
+ changed.each do |change|
+ #should work as has_key(change), but hash == doesn't work on 1.8.6 here for some reason it never matches
+ if total.has_key?(change.to_a.sort)
+ total[change.to_a.sort] += 1
+ else
+ total[change.to_a.sort] = 1
+ end
+ end
+ total
+ end
+
+end
View
97 lib/base/code_issue.rb
@@ -0,0 +1,97 @@
+require 'delegate'
+require MetricFu::LIB_ROOT + '/base/metric_analyzer'
+require MetricFu::LIB_ROOT + '/base/flog_analyzer'
+require MetricFu::LIB_ROOT + '/base/saikuro_analyzer'
+require MetricFu::LIB_ROOT + '/base/churn_analyzer'
+require MetricFu::LIB_ROOT + '/base/reek_analyzer'
+require MetricFu::LIB_ROOT + '/base/flay_analyzer'
+
+module CarefulArray
+
+ def carefully_remove(elements)
+ missing_elements = elements - self
+ raise "Cannot delete missing elements: #{missing_elements.inspect}" unless missing_elements.empty?
+ (self - elements).extend(CarefulArray)
+ end
+
+end
+
+class CodeIssue < DelegateClass(Record) #DelegateClass(Ruport::Data::Record)
+ include Comparable
+
+ # TODO: Yuck! 'stat_value' is a column for StatAnalyzer
+ EXCLUDED_COLUMNS = FlogAnalyzer::COLUMNS + SaikuroAnalyzer::COLUMNS + ['stat_value'] + ChurnAnalyzer::COLUMNS + ReekAnalyzer.new.columns.extend(CarefulArray).carefully_remove(['reek__type_name', 'reek__comparable_message']) + FlayAnalyzer.new.columns.extend(CarefulArray).carefully_remove(['flay_matching_reason'])
+
+ def <=>(other)
+ spaceship_for_columns(self.attributes, other)
+ end
+
+ def ===(other)
+ self.hash_for(included_columns_hash, included_columns) == other.hash_for(included_columns_hash, included_columns)
+ end
+
+ def spaceship_for_columns(columns, other)
+ columns.each do |column|
+ equality = self[column].to_s <=> other[column].to_s
+ return equality if equality!=0
+ end
+ return 0
+ end
+
+ def hash_for(column_hash, columns)
+ @hashes ||= {}
+ # fetch would be cleaner, but slower
+ if @hashes.has_key?(column_hash)
+ @hashes[column_hash]
+ else
+ values = columns.map {|column| self[column]}
+ hash_for_columns = values.join('').hash
+ @hashes[column_hash]=hash_for_columns
+ hash_for_columns
+ end
+ end
+
+ def included_columns_hash
+ @included_columns_hash ||= included_columns.hash
+ end
+
+ def included_columns
+ @included_columns ||= self.attributes.extend(CarefulArray).carefully_remove(EXCLUDED_COLUMNS)
+ end
+
+ def find_counterpart_index_in(collection)
+ # each_with_index is cleaner, but it is slower and we
+ # spend a lot of time in this loop
+ index = 0
+ collection.each do |issue|
+ return index if self === issue
+ index += 1
+ end
+ return nil
+ end
+
+ def modifies?(other)
+ case self.metric
+ when :reek
+ #return false unless ["Large Class", "Long Method", "Long Parameter List"].include?(self.reek__type_name)
+ return false if self.reek__type_name != other.reek__type_name
+ self.reek__value != other.reek__value
+ when :flog
+ self.score != other.score
+ when :saikuro
+ self.complexity != other.complexity
+ when :stats
+ self.stat_value != other.stat_value
+ when :churn
+ self.times_changed != other.times_changed
+ when :flay
+ #self.flay_reason != other.flay_reason
+ # do nothing for now
+ when :roodi, :stats
+ # do nothing
+ else
+ raise ArgumentError, "Invalid metric type #{self.metric}"
+ end
+ end
+
+end
View
68 lib/base/configuration.rb
@@ -5,9 +5,12 @@ module MetricFu
# These are metrics which have been developed for the system. Of
# course, in order to use these metrics, their respective gems must
# be installed on the system.
- AVAILABLE_METRICS = [:churn, :flog, :flay, :reek,
- :roodi, :saikuro, :rcov]
-
+ AVAILABLE_METRICS = [:churn, :flog, :flay, :reek,
+ :roodi, :rcov,
+ :hotspots]
+
+ AVAILABLE_METRICS << :saikuro unless RUBY_VERSION == '1.9.2'
+
AVAILABLE_GRAPHS = [:flog, :flay, :reek, :roodi, :rcov, :rails_best_practices]
AVAILABLE_GRAPH_ENGINES = [:gchart, :bluff]
@@ -35,7 +38,7 @@ def self.configuration
#
# == Customization for CruiseControl.rb
#
- # The Configuration class checks for the presence of a
+ # The Configuration class checks for the presence of a
# 'CC_BUILD_ARTIFACTS' environment variable. If it's found
# it will change the default output directory from the default
# "tmp/metric_fu to the directory represented by 'CC_BUILD_ARTIFACTS'
@@ -44,7 +47,7 @@ def self.configuration
#
# The Configuration class checks for several deprecated constants
# that were previously used to configure MetricFu. These include
- # CHURN_OPTIONS, DIRECTORIES_TO_FLOG, SAIKURO_OPTIONS,
+ # CHURN_OPTIONS, DIRECTORIES_TO_FLOG, SAIKURO_OPTIONS,
# and MetricFu::SAIKURO_OPTIONS.
#
# These have been replaced by config.churn, config.flog and
@@ -56,7 +59,7 @@ def initialize #:nodoc:#
add_attr_accessors_to_self
add_class_methods_to_metric_fu
end
-
+
# Searches through the instance variables of the class and
# creates a class method on the MetricFu module to read the value
# of the instance variable from the Configuration class.
@@ -71,9 +74,9 @@ def self.#{method_name}
MetricFu.module_eval(method)
end
end
-
+
# Searches through the instance variables of the class and creates
- # an attribute accessor on this instance of the Configuration
+ # an attribute accessor on this instance of the Configuration
# class for each instance variable.
def add_attr_accessors_to_self
instance_variables.each do |name|
@@ -92,7 +95,7 @@ def add_attr_accessors_to_self
def self.run
yield MetricFu.configuration
end
-
+
# This does the real work of the Configuration class, by setting
# up a bunch of instance variables to represent the configuration
# of the MetricFu app.
@@ -101,19 +104,23 @@ def reset
@scratch_directory = File.join(@base_directory, 'scratch')
@output_directory = File.join(@base_directory, 'output')
@data_directory = File.join('tmp/metric_fu', '_data')
- @metric_fu_root_directory = File.join(File.dirname(__FILE__),
+ @metric_fu_root_directory = File.join(File.dirname(__FILE__),
'..', '..')
- @template_directory = File.join(@metric_fu_root_directory,
- 'lib', 'templates')
+ @template_directory = File.join(@metric_fu_root_directory,
+ 'lib', 'templates')
@template_class = AwesomeTemplate
set_metrics
set_graphs
set_code_dirs
- @flay = { :dirs_to_flay => @code_dirs }
+ @flay = { :dirs_to_flay => @code_dirs,
+ :minimum_score => 100,
+ :filetypes => ['rb'] }
@flog = { :dirs_to_flog => @code_dirs }
- @reek = { :dirs_to_reek => @code_dirs }
- @roodi = { :dirs_to_roodi => @code_dirs }
- @saikuro = { :output_directory => @scratch_directory + '/saikuro',
+ @reek = { :dirs_to_reek => @code_dirs,
+ :config_file_pattern => nil}
+ @roodi = { :dirs_to_roodi => @code_dirs,
+ :roodi_config => nil}
+ @saikuro = { :output_directory => @scratch_directory + '/saikuro',
:input_directory => @code_dirs,
:cyclo => "",
:filter_cyclo => "0",
@@ -122,19 +129,22 @@ def reset
:formater => "text"}
@churn = {}
@stats = {}
- @rcov = { :test_files => ['test/**/*_test.rb',
+ @rcov = { :environment => 'test',
+ :test_files => ['test/**/*_test.rb',
'spec/**/*_spec.rb'],
- :rcov_opts => ["--sort coverage",
- "--no-html",
+ :rcov_opts => ["--sort coverage",
+ "--no-html",
"--text-coverage",
"--no-color",
"--profile",
"--rails",
- "--exclude /gems/,/Library/,/usr/,spec"]}
+ "--exclude /gems/,/Library/,/usr/,spec"],
+ :external => nil
+ }
@rails_best_practices = {}
-
+ @hotspots = {}
@file_globs_to_ignore = []
-
+
@graph_engine = :bluff # can be :bluff or :gchart
end
@@ -153,11 +163,15 @@ def set_metrics
@metrics = MetricFu::AVAILABLE_METRICS + [:stats, :rails_best_practices]
else
@metrics = MetricFu::AVAILABLE_METRICS
- end
+ end
end
-
+
def set_graphs
- @graphs = MetricFu::AVAILABLE_GRAPHS
+ if rails?
+ @graphs = MetricFu::AVAILABLE_GRAPHS + [:stats]
+ else
+ @graphs = MetricFu::AVAILABLE_GRAPHS
+ end
end
# Add the 'app' directory if we're running within rails.
@@ -168,11 +182,11 @@ def set_code_dirs
@code_dirs = ['lib']
end
end
-
+
def platform #:nodoc:
return RUBY_PLATFORM
end
-
+
def is_cruise_control_rb?
!!ENV['CC_BUILD_ARTIFACTS']
end
View
50 lib/base/flay_analyzer.rb
@@ -0,0 +1,50 @@
+class FlayAnalyzer
+ include ScoringStrategies
+
+ COLUMNS = %w{flay_reason flay_matching_reason}
+
+ def columns
+ COLUMNS
+ end
+
+ def name
+ :flay
+ end
+
+ def map(row)
+ ScoringStrategies.present(row)
+ end
+
+ def reduce(scores)
+ ScoringStrategies.sum(scores)
+ end
+
+ def score(metric_ranking, item)
+ ScoringStrategies.percentile(metric_ranking, item)
+ end
+
+ def generate_records(data, table)
+ return if data==nil
+ Array(data[:matches]).each do |match|
+ problems = match[:reason]
+ matching_reason = problems.gsub(/^[0-9]+\) /,'').gsub(/\:[0-9]+/,'')
+ files = []
+ locations = []
+ match[:matches].each do |file_match|
+ file_path = file_match[:name].sub(%r{^/},'')
+ locations << "#{file_path}:#{file_match[:line]}"
+ files << file_path
+ end
+ files = files.uniq
+ files.each do |file|
+ table << {
+ "metric" => self.name,
+ "file_path" => file,
+ "flay_reason" => problems+" files: #{locations.join(', ')}",
+ "flay_matching_reason" => matching_reason
+ }
+ end
+ end
+ end
+
+end
View
43 lib/base/flog_analyzer.rb
@@ -0,0 +1,43 @@
+class FlogAnalyzer
+ include ScoringStrategies
+
+ COLUMNS = %w{score}
+
+ def columns
+ COLUMNS
+ end
+
+ def name
+ :flog
+ end
+
+ def map(row)
+ row.score
+ end
+
+ def reduce(scores)
+ ScoringStrategies.average(scores)
+ end
+
+ def score(metric_ranking, item)
+ ScoringStrategies.identity(metric_ranking, item)
+ end
+
+ def generate_records(data, table)
+ return if data==nil
+ Array(data[:method_containers]).each do |method_container|
+ Array(method_container[:methods]).each do |entry|
+ file_path = entry[1][:path].sub(%r{^/},'') if entry[1][:path]
+ location = MetricFu::Location.for(entry.first)
+ table << {
+ "metric" => name,
+ "score" => entry[1][:score],
+ "file_path" => file_path,
+ "class_name" => location.class_name,
+ "method_name" => location.method_name
+ }
+ end
+ end
+ end
+
+end
View
49 lib/base/generator.rb
@@ -2,18 +2,18 @@ module MetricFu
# = Generator
#
- # The Generator class is an abstract class that provides the
+ # The Generator class is an abstract class that provides the
# skeleton for producing different types of metrics.
#
- # It drives the production of the metrics through a template
+ # It drives the production of the metrics through a template
# method - #generate_report(options={}). This method calls
# #emit, #analyze and #to_h in order to produce the metrics.
#
# To implement a concrete class to generate a metric, therefore,
- # the class must implement those three methods.
+ # the class must implement those three methods.
#
- # * #emit should take care of running the metric tool and
- # gathering its output.
+ # * #emit should take care of running the metric tool and
+ # gathering its output.
# * #analyze should take care of manipulating the output from
# #emit and making it possible to store it in a programmatic way.
# * #to_h should provide a hash representation of the output from
@@ -33,14 +33,13 @@ class Generator
attr_reader :report, :template
def initialize(options={})
- self.class.verify_dependencies!
create_metric_dir_if_missing
create_output_dir_if_missing
create_data_dir_if_missing
end
-
- # Creates a new generator and returns the output of the
- # #generate_report method. This is the typical way to
+
+ # Creates a new generator and returns the output of the
+ # #generate_report method. This is the typical way to
# generate a new MetricFu report. For more information see
# the #generate_report instance method.
#
@@ -67,27 +66,27 @@ def self.generate_report(options={})
def self.class_name
self.to_s.split('::').last.downcase
end
-
+
# Returns the directory where the Generator will write any output
def self.metric_directory
- File.join(MetricFu.scratch_directory, class_name)
+ File.join(MetricFu.scratch_directory, class_name)
end
def create_metric_dir_if_missing #:nodoc:
unless File.directory?(metric_directory)
- FileUtils.mkdir_p(metric_directory, :verbose => false)
+ FileUtils.mkdir_p(metric_directory, :verbose => false)
end
end
-
+
def create_output_dir_if_missing #:nodoc:
unless File.directory?(MetricFu.output_directory)
- FileUtils.mkdir_p(MetricFu.output_directory, :verbose => false)
+ FileUtils.mkdir_p(MetricFu.output_directory, :verbose => false)
end
end
-
+
def create_data_dir_if_missing #:nodoc:
unless File.directory?(MetricFu.data_directory)
- FileUtils.mkdir_p(MetricFu.data_directory, :verbose => false)
+ FileUtils.mkdir_p(MetricFu.data_directory, :verbose => false)
end
end
@@ -104,7 +103,7 @@ def remove_excluded_files(paths, globs_to_remove = MetricFu.file_globs_to_ignore
end
paths - files_to_remove
end
-
+
# Defines some hook methods for the concrete classes to hook into.
%w[emit analyze].each do |meth|
define_method("before_#{meth}".to_sym) {}
@@ -132,18 +131,14 @@ def generate_report
end
def round_to_tenths(decimal)
- (decimal * 10).round / 10.0
- end
-
- # Allows subclasses to check for required gems
- def self.verify_dependencies!
- true
+ decimal = 0.0 if decimal.to_s.eql?('NaN')
+ (decimal * 10).round / 10.0
end
def emit #:nodoc:
raise <<-EOF
This method must be implemented by a concrete class descending
- from Generator. See generator class documentation for more
+ from Generator. See generator class documentation for more
information.
EOF
end
@@ -151,15 +146,15 @@ def emit #:nodoc:
def analyze #:nodoc:
raise <<-EOF
This method must be implemented by a concrete class descending
- from Generator. See generator class documentation for more
+ from Generator. See generator class documentation for more
information.
EOF
end
-
+
def to_graph #:nodoc:
raise <<-EOF
This method must be implemented by a concrete class descending
- from Generator. See generator class documentation for more
+ from Generator. See generator class documentation for more
information.
EOF
end
View
25 lib/base/graph.rb
@@ -5,35 +5,40 @@ def self.graph
end
class Graph
-
+
attr_accessor :clazz
-
+
def initialize
self.clazz = []
end
-
+
def add(graph_type, graph_engine)
grapher_name = graph_type.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase } + graph_engine.to_s.capitalize + "Grapher"
self.clazz.push MetricFu.const_get(grapher_name).new
end
-
-
+
+
def generate
return if self.clazz.empty?
puts "Generating graphs"
Dir[File.join(MetricFu.data_directory, '*.yml')].sort.each do |metric_file|
puts "Generating graphs for #{metric_file}"
- date = metric_file.split('/')[3].split('.')[0]
- y, m, d = date[0..3].to_i, date[4..5].to_i, date[6..7].to_i
+ date_parts = year_month_day_from_filename(metric_file)
metrics = YAML::load(File.open(metric_file))
-
+
self.clazz.each do |grapher|
- grapher.get_metrics(metrics, "#{m}/#{d}")
+ grapher.get_metrics(metrics, "#{date_parts[:m]}/#{date_parts[:d]}")
end
end
self.clazz.each do |grapher|
grapher.graph!
end
end
+
+ private
+ def year_month_day_from_filename(path_to_file_with_date)
+ date = path_to_file_with_date.match(/\/(\d+).yml$/)[1]
+ {:y => date[0..3].to_i, :m => date[4..5].to_i, :d => date[6..7].to_i}
+ end
end
-end
+end
View
66 lib/base/line_numbers.rb
@@ -0,0 +1,66 @@
+require 'ruby_parser'
+module MetricFu
+ class LineNumbers
+
+ def initialize(contents)
+ rp = RubyParser.new
+ file_sexp = rp.parse(contents)
+ @locations = {}
+ case file_sexp[0]
+ when :class
+ process_class(file_sexp)
+ when :module
+ process_module(file_sexp)
+ when :block
+ file_sexp.each_of_type(:class) { |sexp| process_class(sexp) }
+ else
+ end
+ end
+
+ def in_method? line_number
+ !!@locations.detect do |method_name, line_number_range|
+ line_number_range.include?(line_number)
+ end
+ end
+
+ def method_at_line line_number
+ found_method_and_range = @locations.detect do |method_name, line_number_range|
+ line_number_range.include?(line_number)
+ end
+ return nil unless found_method_and_range
+ found_method_and_range.first
+ end
+
+ private
+
+ def process_module(sexp)
+ module_name = sexp[1]
+ sexp.each_of_type(:class) do |sexp|
+ process_class(sexp, module_name)
+ hide_methods_from_next_round(sexp)
+ end
+ process_class(sexp)
+ end
+
+ def process_class(sexp, module_name=nil)
+ class_name = sexp[1]
+ process_class_self_blocks(sexp, class_name)
+ module_name_string = module_name ? "#{module_name}::" : nil
+ sexp.each_of_type(:defn) { |s| @locations["#{module_name_string}#{class_name}##{s[1]}"] = (s.line)..(s.last.line) }
+ sexp.each_of_type(:defs) { |s| @locations["#{module_name_string}#{class_name}::#{s[2]}"] = (s.line)..(s.last.line) }
+ end
+
+ def process_class_self_blocks(sexp, class_name)
+ sexp.each_of_type(:sclass) do |sexp_in_class_self_block|
+ sexp_in_class_self_block.each_of_type(:defn) { |s| @locations["#{class_name}::#{s[1]}"] = (s.line)..(s.last.line) }
+ hide_methods_from_next_round(sexp_in_class_self_block)
+ end
+ end
+
+ def hide_methods_from_next_round(sexp)
+ sexp.find_and_replace_all(:defn, :ignore_me)
+ sexp.find_and_replace_all(:defs, :ignore_me)
+ end
+
+ end
+end
View
85 lib/base/location.rb
@@ -0,0 +1,85 @@
+module MetricFu
+ class Location
+ include Comparable
+
+ attr_accessor :class_name, :method_name, :file_path, :simple_method_name, :hash
+
+ def self.get(file_path, class_name, method_name)
+ # This could be more 'confident' using Maybe, but we want it to be as fast as possible
+ file_path_copy = file_path == nil ? nil : file_path.clone
+ class_name_copy = class_name == nil ? nil : class_name.clone
+ method_name_copy = method_name == nil ? nil : method_name.clone
+ key = [file_path_copy, class_name_copy, method_name_copy]
+ @@locations ||= {}
+ if @@locations.has_key?(key)
+ @@locations[key]
+ else
+ location = self.new(file_path_copy, class_name_copy, method_name_copy)
+ @@locations[key] = location
+ location.freeze # we cache a lot of method call results, so we want location to be immutable
+ location
+ end
+ end
+
+ def initialize(file_path, class_name, method_name)
+ @file_path = file_path
+ @class_name = class_name
+ @method_name = method_name
+ @simple_method_name = @method_name.sub(@class_name,'') unless @method_name == nil
+ @hash = [@file_path, @class_name, @method_name].hash
+ end
+
+ # TODO - we need this method (and hash accessor above) as a temporary hack where we're using Location as a hash key
+ def eql?(other)
+ [self.file_path.to_s, self.class_name.to_s, self.method_name.to_s] == [other.file_path.to_s, other.class_name.to_s, other.method_name.to_s]
+ end
+ # END we need these methods as a temporary hack where we're using Location as a hash key
+
+ def self.for(class_or_method_name)
+ class_or_method_name = strip_modules(class_or_method_name)
+ if(class_or_method_name)
+ begin
+ match = class_or_method_name.match(/(.*)((\.|\#|\:\:[a-z])(.+))/)
+ rescue => error
+ #new error during port to metric_fu occasionally a unintialized
+ #MatchData object shows up here. Not expected.
+ match = nil
+ end
+
+ # reek reports the method with :: not # on modules like
+ # module ApplicationHelper \n def signed_in?, convert it so it records correctly
+ # but classes have to start with a capital letter... HACK for REEK bug, reported underlying issue to REEK
+ if(match)
+ class_name = strip_modules(match[1])
+ method_name = class_or_method_name.gsub(/\:\:/,"#")
+ else
+ class_name = strip_modules(class_or_method_name)
+ method_name = nil
+ end
+ else
+ class_name = nil
+ method_name = nil
+ end
+ self.get(nil, class_name, method_name)
+ end
+
+ def <=>(other)
+ [self.file_path.to_s, self.class_name.to_s, self.method_name.to_s] <=> [other.file_path.to_s, other.class_name.to_s, other.method_name.to_s]
+ end
+
+ private
+
+ def self.strip_modules(class_or_method_name)
+ # reek reports the method with :: not # on modules like
+ # module ApplicationHelper \n def signed_in?, convert it so it records correctly
+ # but classes have to start with a capital letter... HACK for REEK bug, reported underlying issue to REEK
+ if(class_or_method_name=~/\:\:[A-Z]/)
+ class_or_method_name.split("::").last
+ else
+ class_or_method_name
+ end
+
+ end
+
+ end
+end
View
404 lib/base/metric_analyzer.rb
@@ -0,0 +1,404 @@
+class AnalysisError < RuntimeError; end;
+
+class MetricAnalyzer
+
+ COMMON_COLUMNS = %w{metric}
+ GRANULARITIES = %w{file_path class_name method_name}
+
+ attr_accessor :table
+
+ def initialize(yaml)
+ if(yaml.is_a?(String))
+ @yaml = YAML.load(yaml)
+ else
+ @yaml = yaml
+ end
+ @file_ranking = MetricFu::Ranking.new
+ @class_ranking = MetricFu::Ranking.new
+ @method_ranking = MetricFu::Ranking.new
+ rankings = [@file_ranking, @class_ranking, @method_ranking]
+
+ tool_analyzers = [ReekAnalyzer.new, RoodiAnalyzer.new,
+ FlogAnalyzer.new, ChurnAnalyzer.new, SaikuroAnalyzer.new,
+ FlayAnalyzer.new, StatsAnalyzer.new, RcovAnalyzer.new]
+ # TODO There is likely a clash that will happen between
+ # column names eventually. We should probably auto-prefix
+ # them (e.g. "roodi_problem")
+ columns = COMMON_COLUMNS + GRANULARITIES + tool_analyzers.map{|analyzer| analyzer.columns}.flatten
+
+ @table = make_table(columns)
+
+ # These tables are an optimization. They contain subsets of the master table.
+ # TODO - these should be pushed into the Table class now
+ @tool_tables = make_table_hash(columns)
+ @file_tables = make_table_hash(columns)
+ @class_tables = make_table_hash(columns)
+ @method_tables = make_table_hash(columns)
+
+ tool_analyzers.each do |analyzer|
+ analyzer.generate_records(@yaml[analyzer.name], @table)
+ end
+
+ build_lookups!(table)
+ process_rows!(table)
+
+ tool_analyzers.each do |analyzer|
+ GRANULARITIES.each do |granularity|
+ metric_ranking = calculate_metric_scores(granularity, analyzer)
+ add_to_master_ranking(ranking(granularity), metric_ranking, analyzer)
+ end
+ end
+
+ rankings.each do |ranking|
+ ranking.delete(nil)
+ end
+ end
+
+ def location(item, value)
+ sub_table = get_sub_table(item, value)
+ if(sub_table.length==0)
+ raise AnalysisError, "The #{item.to_s} '#{value.to_s}' does not have any rows in the analysis table"
+ else
+ first_row = sub_table[0]
+ case item
+ when :class
+ MetricFu::Location.get(first_row.file_path, first_row.class_name, nil)
+ when :method
+ MetricFu::Location.get(first_row.file_path, first_row.class_name, first_row.method_name)
+ when :file
+ MetricFu::Location.get(first_row.file_path, nil, nil)
+ else
+ raise ArgumentError, "Item must be :class, :method, or :file"
+ end
+ end
+ end
+
+ #todo redo as item,value, options = {}
+ # Note that the other option for 'details' is :detailed (this isn't
+ # at all clear from this method itself
+ def problems_with(item, value, details = :summary, exclude_details = [])
+ sub_table = get_sub_table(item, value)
+ #grouping = Ruport::Data::Grouping.new(sub_table, :by => 'metric')
+ grouping = get_grouping(sub_table, :by => 'metric')
+ problems = {}
+ grouping.each do |metric, table|
+ if details == :summary || exclude_details.include?(metric)
+ problems[metric] = present_group(metric,table)
+ else
+ problems[metric] = present_group_details(metric,table)
+ end
+ end
+ problems
+ end
+
+ def worst_methods(size = nil)
+ @method_ranking.top(size)
+ end
+
+ def worst_classes(size = nil)
+ @class_ranking.top(size)
+ end
+
+ def worst_files(size = nil)
+ @file_ranking.top(size)
+ end
+
+ private
+
+ def get_grouping(table, opts)
+ #Ruport::Data::Grouping.new(table, opts)
+ Grouping.new(table, opts)
+ #@grouping_cache ||= {}
+ #@grouping_cache.fetch(grouping_key(table,opts)) do
+ # @grouping_cache[grouping_key(table,opts)] = Ruport::Data::Grouping.new(table, opts)
+ #end
+ end
+
+ def grouping_key(table, opts)
+ "table #{table.object_id} opts #{opts.inspect}"
+ end
+
+ def build_lookups!(table)
+ @class_and_method_to_file ||= {}
+ # Build a mapping from [class,method] => filename
+ # (and make sure the mapping is unique)
+ table.each do |row|
+ # We know that Saikuro provides the wrong data
+ next if row['metric'] == :saikuro
+ key = [row['class_name'], row['method_name']]
+ file_path = row['file_path']
+ @class_and_method_to_file[key] ||= file_path
+ end
+ end
+
+ def process_rows!(table)
+ # Correct incorrect rows in the table
+ table.each do |row|
+ row_metric = row['metric'] #perf optimization
+ if row_metric == :saikuro
+ fix_row_file_path!(row)
+ end
+ @tool_tables[row_metric] << row
+ @file_tables[row["file_path"]] << row
+ @class_tables[row["class_name"]] << row
+ @method_tables[row["method_name"]] << row
+ end
+ end
+
+ def fix_row_file_path!(row)
+ # We know that Saikuro rows are broken
+ # next unless row['metric'] == :saikuro
+ key = [row['class_name'], row['method_name']]
+ current_file_path = row['file_path'].to_s
+ correct_file_path = @class_and_method_to_file[key]
+ if(correct_file_path!=nil && correct_file_path.include?(current_file_path))
+ row['file_path'] = correct_file_path
+ else
+ # There wasn't an exact match, so we can do a substring match
+ matching_file_path = file_paths.detect {|file_path|
+ file_path!=nil && file_path.include?(current_file_path)
+ }
+ if(matching_file_path)
+ row['file_path'] = matching_file_path
+ end
+ end
+ end
+
+ def file_paths
+ @file_paths ||= @table.column('file_path').uniq
+ end
+
+ def ranking(column_name)
+ case column_name
+ when "file_path"
+ @file_ranking
+ when "class_name"
+ @class_ranking
+ when "method_name"
+ @method_ranking
+ else
+ raise ArgumentError, "Invalid column name #{column_name}"
+ end
+ end
+
+ def calculate_metric_scores(granularity, analyzer)
+ metric_ranking = MetricFu::Ranking.new
+ metric_violations = @tool_tables[analyzer.name]
+ metric_violations.each do |row|
+ location = row[granularity]
+ metric_ranking[location] ||= []
+ metric_ranking[location] << analyzer.map(row)
+ end
+
+ metric_ranking.each do |item, scores|
+ metric_ranking[item] = analyzer.reduce(scores)
+ end
+
+ metric_ranking
+ end
+
+ def add_to_master_ranking(master_ranking, metric_ranking, analyzer)
+ metric_ranking.each do |item, _|
+ master_ranking[item] ||= 0
+ master_ranking[item] += analyzer.score(metric_ranking, item) # scaling? Do we just add in the raw score?
+ end
+ end
+
+ def most_common_column(column_name, size)
+ #grouping = Ruport::Data::Grouping.new(@table,
+ # :by => column_name,
+ # :order => lambda { |g| -g.size})
+ get_grouping(@table, :by => column_name, :order => lambda {|g| -g.size})
+ values = []
+ grouping.each do |value, _|
+ values << value if value!=nil
+ if(values.size==size)
+ break
+ end
+ end
+ return nil if values.empty?
+ if(values.size == 1)
+ return values.first
+ else
+ return values
+ end
+ end
+
+ # TODO: As we get fancier, the presenter should
+ # be its own class, not just a method with a long
+ # case statement
+ def present_group(metric, group)
+ occurences = group.size
+ case(metric)
+ when :reek
+ "found #{occurences} code smells"
+ when :roodi
+ "found #{occurences} design problems"
+ when :churn
+ "detected high level of churn (changed #{group[0].times_changed} times)"
+ when :flog
+ complexity = get_mean(group.column("score"))
+ "#{"average " if occurences > 1}complexity is %.1f" % complexity
+ when :saikuro
+ complexity = get_mean(group.column("complexity"))
+ "#{"average " if occurences > 1}complexity is %.1f" % complexity
+ when :flay
+ "found #{occurences} code duplications"
+ when :rcov
+ average_code_uncoverage = get_mean(group.column("percentage_uncovered"))
+ "#{"average " if occurences > 1}uncovered code is %.1f%" % average_code_uncoverage
+ else
+ raise AnalysisError, "Unknown metric #{metric}"
+ end
+ end
+
+ def present_group_details(metric, group)
+ occurences = group.size
+ case(metric)
+ when :reek
+ message = "found #{occurences} code smells<br/>"
+ group.each do |item|
+ type = item.data["reek__type_name"]
+ reek_message = item.data["reek__message"]
+ message << "* #{type}: #{reek_message}<br/>"
+ end
+ message
+ when :roodi
+ message = "found #{occurences} design problems<br/>"
+ group.each do |item|
+ problem = item.data["problems"]
+ message << "* #{problem}<br/>"
+ end
+ message
+ when :churn
+ "detected high level of churn (changed #{group[0].times_changed} times)"
+ when :flog
+ complexity = get_mean(group.column("score"))
+ "#{"average " if occurences > 1}complexity is %.1f" % complexity
+ when :saikuro
+ complexity = get_mean(group.column("complexity"))
+ "#{"average " if occurences > 1}complexity is %.1f" % complexity
+ when :flay
+ message = "found #{occurences} code duplications<br/>"
+ group.each do |item|
+ problem = item.data["flay_reason"]
+ problem = problem.gsub(/^[0-9]*\)/,'')
+ problem = problem.gsub(/files\:/,' <br>&nbsp;&nbsp;&nbsp;files:')
+ message << "* #{problem}<br/>"
+ end
+ message
+ else
+ raise AnalysisError, "Unknown metric #{metric}"
+ end
+ end
+
+ def make_table_hash(columns)
+ Hash.new { |hash, key|
+ hash[key] = make_table(columns)
+ }
+ end
+
+ def make_table(columns)
+ Table.new(:column_names => columns)
+ end
+
+ def get_sub_table(item, value)
+ tables = {
+ :class => @class_tables,
+ :method => @method_tables,
+ :file => @file_tables,
+ :tool => @tool_tables
+ }.fetch(item) do
+ raise ArgumentError, "Item must be :class, :method, or :file"
+ end
+ tables[value]
+ end
+
+ def get_mean(collection)
+ collection_length = collection.length
+ sum = 0
+ sum = collection.inject( nil ) { |sum,x| sum ? sum+x : x }
+ (sum.to_f / collection_length.to_f)
+ end
+
+end
+
+class Record
+
+ attr_reader :data
+
+ def initialize(data, columns)
+ @data = data
+ @columns = columns
+ end
+
+ def method_missing(name, *args, &block)
+ key = name.to_s
+ if @data.has_key?(key)
+ @data[key]
+ elsif @columns.member?(key)
+ nil
+ else
+ super(name, *args, &block)
+ end
+ end
+
+ def []=(key, value)
+ @data[key]=value
+ end
+
+ def [](key)
+ @data[key]
+ end
+
+ def keys
+ @data.keys
+ end
+
+ def has_key?(key)
+ @data.has_key?(key)
+ end
+
+ def attributes
+ @columns
+ end
+
+end
+
+class Grouping
+
+ def initialize(table, opts)
+ column_name = opts.fetch(:by)
+ order = opts.fetch(:order) { nil }
+ hash = {}
+ if column_name.to_sym == :metric # special optimized case
+ hash = table.group_by_metric
+ else
+ table.each do |row|
+ hash[row[column_name]] ||= Table.new(:column_names => row.attributes)
+ hash[row[column_name]] << row
+ end
+ end
+ if order
+ @arr = hash.sort_by &order
+ else
+ @arr = hash.to_a
+ end
+ end
+
+ def [](key)
+ @arr.each do |group_key, table|
+ return table if group_key == key
+ end
+ return nil
+ end
+
+ def each
+ @arr.each do |value, rows|
+ yield value, rows
+ end
+ end
+
+end
+
+
View
34 lib/base/ranking.rb
@@ -0,0 +1,34 @@
+require 'forwardable'
+module MetricFu
+ class Ranking
+ extend Forwardable
+
+ def initialize
+ @items_to_score = {}
+ end
+
+ def top(num=nil)
+ if(num.is_a?(Numeric))
+ sorted_items[0,num]
+ else
+ sorted_items
+ end
+ end
+
+ def percentile(item)
+ index = sorted_items.index(item)
+ worse_item_count = (length - (index+1))
+ worse_item_count.to_f/length
+ end
+
+ def_delegator :@items_to_score, :has_key?, :scored?
+ def_delegators :@items_to_score, :[], :[]=, :length, :each, :delete
+
+ private
+
+ def sorted_items
+ @sorted_items ||= @items_to_score.sort_by {|item, score| -score}.map {|item, score| item}
+ end
+
+ end
+end
View
43 lib/base/rcov_analyzer.rb
@@ -0,0 +1,43 @@
+class RcovAnalyzer
+ include ScoringStrategies
+
+ COLUMNS = %w{percentage_uncovered}
+
+ def columns
+ COLUMNS
+ end
+
+ def name
+ :rcov
+ end
+
+ def map(row)
+ row.percentage_uncovered
+ end
+
+ def reduce(scores)
+ ScoringStrategies.average(scores)
+ end
+
+ def score(metric_ranking, item)
+ ScoringStrategies.identity(metric_ranking, item)
+ end
+
+ def generate_records(data, table)
+ return if data==nil
+ data.each do |file_name, info|
+ next if (file_name == :global_percent_run) || (info[:methods].nil?)
+ info[:methods].each do |method_name, percentage_uncovered|
+ location = MetricFu::Location.for(method_name)
+ table << {
+ "metric" => :rcov,
+ 'file_path' => file_name,
+ 'class_name' => location.class_name,
+ "method_name" => location.method_name,
+ "percentage_uncovered" => percentage_uncovered
+ }
+ end
+ end
+ end
+
+end
View
116 lib/base/reek_analyzer.rb
@@ -0,0 +1,116 @@
+# coding: utf-8
+
+class ReekAnalyzer
+ include ScoringStrategies
+
+ REEK_ISSUE_INFO = {'Uncommunicative Name' =>
+ {'link' => 'http://wiki.github.com/kevinrutherford/reek/uncommunicative-name', 'info' => 'An Uncommunicative Name is a name that doesn’t communicate its intent well enough.'},
+ 'Class Variable' =>
+ {'link' => 'http://wiki.github.com/kevinrutherford/reek/class-variable', 'info' => 'Class variables form part of the global runtime state, and as such make it easy for one part of the system to accidentally or inadvertently depend on another part of the system.'},
+ 'Duplication' => {'link' =>'http://wiki.github.com/kevinrutherford/reek/duplication', 'info' => 'Duplication occurs when two fragments of code look nearly identical, or when two fragments of code have nearly identical effects at some conceptual level.'},
+ 'Low Cohesion' => {'link' => 'http://en.wikipedia.org/wiki/Cohesion_(computer_science)', 'info' => 'Low cohesion is associated with undesirable traits such as being difficult to maintain, difficult to test, difficult to reuse, and even difficult to understand.'},
+ 'Nested Iterators' => {'link' =>'http://wiki.github.com/kevinrutherford/reek/nested-iterators', 'info' => 'Nested Iterator occurs when a block contains another block.'},
+ 'Control Couple' => {'link' =>'http://wiki.github.com/kevinrutherford/reek/control-couple', 'info' => 'Control coupling occurs when a method or block checks the value of a parameter in order to decide which execution path to take. The offending parameter is often called a “Control Couple”.'},
+ 'Irresponsible Module' => {'link' =>'http://wiki.github.com/kevinrutherford/reek/irresponsible-module', 'info' => 'Classes and modules are the units of reuse and release. It is therefore considered good practice to annotate every class and module with a brief comment outlining its responsibilities.'},
+ 'Long Parameter List' => {'link' =>'http://wiki.github.com/kevinrutherford/reek/long-parameter-list', 'info' => 'A Long Parameter List occurs when a method has more than one or two parameters, or when a method yields more than one or two objects to an associated block.'},
+ 'Data Clump' => {'link' =>'http://wiki.github.com/kevinrutherford/reek/data-clump', 'info' => 'In general, a Data Clump occurs when the same two or three items frequently appear together in classes and parameter lists, or when a group of instance variable names start or end with similar substrings.'},
+ 'Simulated Polymorphism' => {'link' =>'http://wiki.github.com/kevinrutherford/reek/simulated-polymorphism', 'info' => 'Simulated Polymorphism occurs when, code uses a case statement (especially on a type field) or code uses instance_of?, kind_of?, is_a?, or === to decide what code to execute'},
+ 'Large Class' => {'link' =>'http://wiki.github.com/kevinrutherford/reek/large-class', 'info' => 'A Large Class is a class or module that has a large number of instance variables, methods or lines of code in any one piece of its specification.'},
+ 'Long Method' => {'link' =>'http://wiki.github.com/kevinrutherford/reek/long-method', 'info' => 'Long methods can be hard to read and understand. They often are harder to test and maintain as well, which can lead to buggier code.'},
+ 'Feature Envy' => {'link' =>'http://wiki.github.com/kevinrutherford/reek/feature-envy', 'info' => 'Feature Envy occurs when a code fragment references another object more often than it references itself, or when several clients do the same series of manipulations on a particular type of object.'},
+ 'Utility Function' => {'link' =>'http://wiki.github.com/kevinrutherford/reek/utility-function', 'info' => 'A Utility Function is any instance method that has no dependency on the state of the instance. It reduces the code’s ability to communicate intent. Code that “belongs” on one class but which is located in another can be hard to find.'},
+ 'Attribute' => {'link' => 'http://wiki.github.com/kevinrutherford/reek/attribute', 'info' => 'A class that publishes a getter or setter for an instance variable invites client classes to become too intimate with its inner workings, and in particular with its representation of state.'}
+ }
+
+ # Note that in practice, the prefix reek__ is appended to each one
+ # This was a partially implemented idea to avoid column name collisions
+ # but it is only done in the ReekAnalyzer
+ COLUMNS = %w{type_name message value value_description comparable_message}
+
+ def self.issue_link(issue)
+ REEK_ISSUE_INFO[issue]
+ end
+
+ def columns
+ COLUMNS.map{|column| "#{name}__#{column}"}
+ end
+
+ def name
+ :reek
+ end
+
+ def map(row)
+ ScoringStrategies.present(row)
+ end
+
+ def reduce(scores)
+ ScoringStrategies.sum(scores)
+ end
+
+ def score(metric_ranking, item)
+ ScoringStrategies.percentile(metric_ranking, item)
+ end
+
+ def generate_records(data, table)
+ return if data==nil
+ data[:matches].each do |match|
+ file_path = match[:file_path]
+ match[:code_smells].each do |smell|
+ location = MetricFu::Location.for(smell[:method])
+ smell_type = smell[:type]
+ message = smell[:message]
+ table << {
+ "metric" => name, # important
+ "file_path" => file_path, # important
+ # NOTE: ReekAnalyzer is currently different than other analyzers with regard
+ # to column name. Note the COLUMNS constant and #columns method
+ "reek__message" => message,
+ "reek__type_name" => smell_type,
+ "reek__value" => parse_value(message),
+ "reek__value_description" => build_value_description(smell_type, message),
+ "reek__comparable_message" => comparable_message(smell_type, message),
+ "class_name" => location.class_name, # important
+ "method_name" => location.method_name, # important
+ }
+ end
+ end
+ end
+
+ def self.numeric_smell?(type)
+ ["Large Class", "Long Method", "Long Parameter List"].include?(type)
+ end
+
+ private
+
+ def comparable_message(type_name, message)
+ if self.class.numeric_smell?(type_name)
+ match = message.match(/\d+/)
+ if(match)
+ match.pre_match + match.post_match
+ else
+ message
+ end
+ else
+ message
+ end
+ end
+
+ def build_value_description(type_name, message)
+ item_type = message.match(/\d+ (.*)$/)
+ if(item_type)
+ "number of #{item_type[1]} in #{type_name.downcase}"
+ else
+ nil
+ end
+ end
+
+ def parse_value(message)
+ match = message.match(/\d+/)
+ if(match)
+ match[0].to_i
+ else
+ nil
+ end
+ end
+
+end
View
18 lib/base/report.rb
@@ -10,25 +10,25 @@ def self.report
#
# The Report class is responsible two things:
#
- # It adds information to the yaml report, produced by the system
+ # It adds information to the yaml report, produced by the system
# as a whole, for each of the generators used in this test run.
#
# It also handles passing the information from each generator used
- # in this test run out to the template class set in
+ # in this test run out to the template class set in
# MetricFu::Configuration.
class Report
-
+
# Renders the result of the report_hash into a yaml serialization
# ready for writing out to a file.
#
# @return YAML
- # A YAML object containing the results of the report generation
+ # A YAML object containing the results of the report generation
# process
def to_yaml
report_hash.to_yaml
end
-
+
def report_hash #:nodoc:
@report_hash ||= {}
end
@@ -43,7 +43,7 @@ def save_templatized_report
@template.report = report_hash
@template.write
end
-
+
# Adds a hash from a passed report, produced by one of the Generator
# classes to the aggregate report_hash managed by this hash.
#
@@ -55,7 +55,7 @@ def add(report_type)
end
# Saves the passed in content to the passed in directory. If
- # a filename is passed in it will be used as the name of the
+ # a filename is passed in it will be used as the name of the
# file, otherwise it will default to 'index.html'
#
# @param content String
@@ -76,7 +76,7 @@ def save_output(content, dir, file='index.html')
# Checks to discover whether we should try and open the results
# of the report in the browser on this system. We only try and open
- # in the browser if we're on OS X and we're not running in a
+ # in the browser if we're on OS X and we're not running in a
# CruiseControl.rb environment. See MetricFu.configuration for more
# details about how we make those guesses.
#
@@ -91,7 +91,7 @@ def open_in_browser?
# if we're able to open the browser on this platform.
#
# @param dir String
- # The directory path where the 'index.html' we want to open is
+ # The directory path where the 'index.html' we want to open is
# stored
def show_in_browser(dir)
system("open #{dir}/index.html") if open_in_browser?
View
37 lib/base/roodi_analyzer.rb
@@ -0,0 +1,37 @@
+class RoodiAnalyzer
+ include ScoringStrategies
+
+ COLUMNS = %w{problems}
+
+ def columns
+ COLUMNS
+ end
+
+ def name
+ :roodi
+ end
+
+ def map(row)
+ ScoringStrategies.present(row)
+ end
+
+ def reduce(scores)
+ ScoringStrategies.sum(scores)
+ end
+
+ def score(metric_ranking, item)
+ ScoringStrategies.percentile(metric_ranking, item)
+ end
+
+ def generate_records(data, table)
+ return if data==nil
+ Array(data[:problems]).each do |problem|
+ table << {
+ "metric" => name,
+ "problems" => problem[:problem],
+ "file_path" => problem[:file]
+ }
+ end