Code Quality Analysis

13,535 views

Published on

Published in: Technology
  • Be the first to comment

Code Quality Analysis

  1. 1. Code Quality Analysis Marty Andrews @martinjandrews .com.au
  2. 2. static analysis = software analysis without execution
  3. 3. no app runtime required!
  4. 4. feedback!
  5. 5. design improvement improve testability manage complexity
  6. 6. Whole Team Collective Coding Ownership Standard Test-Driven Development Customer Pair Design Planning Tests Programming Improvement Game Simple Design Continuous Sustainable Integration Pace Metaphor Small Releases circa 2001. duplicated from xprogramming.com
  7. 7. Collective Coding Ownership Standard Test-Driven Development Pair Design Programming Improvement Simple Design Continuous Sustainable Integration Pace circa 2001. duplicated from xprogramming.com
  8. 8. smell the code
  9. 9. “A code smell is a surface indication that usually corresponds to a deeper problem in the system.” -- Martin Fowler, paraphrasing Kent Beck
  10. 10. “If it stinks, change it.” -- Grandma Beck
  11. 11. Tools
  12. 12. code samples from runwayapp.com
  13. 13. rake stats
  14. 14. Summary statistics from your Rails app
  15. 15. Look for a good code to test ratio
  16. 16. $ sudo gem install rails $ rails my_app $ cd my_app $ rake stats
  17. 17. ~/Data/runway $ rake stats (in /Users/marty/Data/runway) +----------------------+-------+-------+---------+---------+-----+-------+ | Name | Lines | LOC | Classes | Methods | M/C | LOC/M | +----------------------+-------+-------+---------+---------+-----+-------+ | Controllers | 333 | 275 | 12 | 45 | 3| 4| | Helpers | 113 | 104 | 0| 10 | 0| 8| | Models | 828 | 681 | 12 | 96 | 8| 5| | Libraries | 874 | 750 | 24 | 113 | 4| 4| | Model specs | 1662 | 1395 | 0| 0| 0| 0| | Controller specs | 544 | 434 | 0| 5| 0| 84 | | Helper specs | 114 | 95 | 0| 0| 0| 0| +----------------------+-------+-------+---------+---------+-----+-------+ | Total | 4468 | 3734 | 48 | 269 | 5| 11 | +----------------------+-------+-------+---------+---------+-----+-------+ Code LOC: 1810 Test LOC: 1924 Code to Test Ratio: 1:1.1
  18. 18. 681 | 12 | 96 | 8| 5 750 | 24 | 113 | 4| 4 1395 | 0| 0| 0| 0 434 | 0| 5| 0| 84 95 | 0| 0| 0| 0 -----+---------+---------+-----+------- 3734 | 48 | 269 | 5| 11 -----+---------+---------+-----+------- 4 Code to Test Ratio: 1:1.1
  19. 19. use rake stats for regular human review
  20. 20. reek http://wiki.github.com/kevinrutherford/reek
  21. 21. Searches for code smells
  22. 22. Control Couple Long Parameter List Duplication Nested Iterators Feature Envy Uncommunicative Name Large Class Utility Function Long Method
  23. 23. Look for warnings that you haven’t thought about
  24. 24. $ sudo gem install reek $ reek [file ...]
  25. 25. ~/Data/runway $ find app -name quot;*.rbquot; | xargs reek quot;app/controllers/actions_controller.rbquot; -- 3 warnings: ActionsController#action calls current_user.actions multiple times (Duplication) ActionsController#action calls params[id] multiple times (Duplication) ActionsController#actions calls current_user.actions multiple times (Duplication) quot;app/controllers/application.rbquot; -- 1 warnings: ApplicationController#site_version doesn't depend on instance state (Utility Function) quot;app/helpers/home_helper.rbquot; -- 1 warnings: HomeHelper::folder_tab has approx 6 statements (Long Method) quot;app/models/token.rbquot; -- 21 warnings: Token#parse_incubation_day_of_the_month refers to base more than self (Feature Envy)
  26. 26. ActionsController#action calls current_user.actions multiple times (Duplication) ActionsController#action calls params[id] multiple times (Duplication) ActionsController#actions calls current_user.actions multiple times (Duplication) quot;app/controllers/application.rbquot; -- 1 warnings: ApplicationController#site_version doesn't depend on instance state (Utility Function) quot;app/helpers/home_helper.rbquot; -- 1 warnings: HomeHelper::folder_tab has approx 6 statements (Long Method) quot;app/models/token.rbquot; -- 21 warnings: Token#parse_incubation_day_of_the_month refers to base more than self (Feature Envy)
  27. 27. [Feature Envy] Element#update_from refers to response more than self def parse_incubation_day_of_the_month if @remainder =~ /^(d+)(th|st|nd|rd)$/i day = $1.to_i base = day <= @today.day ? @today >> 1 : @today day -=1 until Date.valid_civil?(base.year, base.month, day) Date.new(base.year, base.month, day) end end
  28. 28. What about Rails?
  29. 29. 10,645 warnings
  30. 30. use reek for regular human review
  31. 31. Flog http://ruby.sadi.st/Flog.html
  32. 32. Complexity checking of Ruby code
  33. 33. “ABC” like algorithm Assignments Branches Conditionals
  34. 34. Look for high scores (> 40)
  35. 35. $ sudo gem install flog $ flog [dir ...]
  36. 36. ~/Data/runway $ flog app 1426.9: flog total 7.9: flog/method average 89.7: ActionFormat#format 81.7: ActionParser#parse 60.4: Token#parse_incubation_date 42.3: Token#parse_incubation_days 35.2: Token#tokenize 29.8: Token#parse 27.8: Token#parse_period 27.6: Action#apply_defaults_from_name 25.3: Action#none 24.9: RankingsController#update 24.2: ActionsHelper#none 22.9: ActionsHelper#action_contexts 21.9: ActionsHelper#action_tags 20.9: Token#parse_incubation_month 20.2: ActionParser#attributes [More content removed...]
  37. 37. ~/Data/runway $ flog app 1426.9: flog total 7.9: flog/method average 89.7: ActionFormat#format 81.7: ActionParser#parse 60.4: Token#parse_incubation_date 42.3: Token#parse_incubation_days 35.2: Token#tokenize 29.8: Token#parse 27.8: Token#parse_period
  38. 38. 89.7: ActionFormat#format def format @tokens = [] @semi = false append(quot;Waiting forquot;) if @action.waiting? append(@action.name) unless @action.name.blank? append_with_semi(@action.due_at.strftime(quot;<%d/%m/%Y-%H:%Mquot;)) if @action.solid? if @action.incubating? append_with_semi(@action.effective_at.strftime(quot;+%d/%m/%Y-%H:%Mquot;)) elsif [quot;laterquot;, quot;donequot;].include?(@action.status) append_with_semi(quot;+#{@action.status}quot;) end append_with_semi(@action.contexts.sort.map(&:shorthand)) unless @action.contexts.blank? unless @action.waiting? append_with_semi(@action.time.shorthand) unless @action.time.blank? append_with_semi(@action.energy.shorthand) unless @action.energy.blank? end append_with_semi(@action.tags.sort.map(&:shorthand)) unless @action.tags.blank? append(quot;!quot;) if @action.today? @tokens.flatten.join(quot; quot;) end
  39. 39. What about Rails?
  40. 40. ~/Data/rails $ flog . 201079.7: flog total 14.1: flog/method average 3841.5: main#none 866.1: Parser#none 709.7: timezone#Europe/London 707.5: timezone#America/St_Johns 695.0: timezone#America/Chicago 693.3: timezone#America/New_York 674.3: timezone#Europe/Dublin 670.9: timezone#America/Halifax 662.2: JoinAssociation#association_join 661.2: timezone#Atlantic/Azores 656.7: timezone#Europe/Lisbon 556.3: timezone#Europe/Brussels [More content removed...]
  41. 41. 841.5: main#none 866.1: Parser#none 709.7: timezone#Europe/London 707.5: timezone#America/St_Johns 695.0: timezone#America/Chicago 693.3: timezone#America/New_York 674.3: timezone#Europe/Dublin 670.9: timezone#America/Halifax 662.2: JoinAssociation#association_join 661.2: timezone#Atlantic/Azores 656.7: timezone#Europe/Lisbon 556.3: timezone#Europe/Brussels [More content removed...]
  42. 42. def association_join connection = reflection.active_record.connection join = case reflection.macro when :has_and_belongs_to_many quot; #{join_type} %s ON %s.%s = %s.%s quot; % [ table_alias_for(options[:join_table], aliased_join_table_name), connection.quote_table_name(aliased_join_table_name), options[:foreign_key] || reflection.active_record.to_s.foreign_key, connection.quote_table_name(parent.aliased_table_name), reflection.active_record.primary_key] + quot; #{join_type} %s ON %s.%s = %s.%s quot; % [ table_name_and_alias, connection.quote_table_name(aliased_table_name), klass.primary_key, connection.quote_table_name(aliased_join_table_name), options[:association_foreign_key] || klass.to_s.foreign_key ] when :has_many, :has_one case when reflection.options[:through] through_conditions = through_reflection.options[:conditions] ? quot;AND #{interpolate_s jt_foreign_key = jt_as_extra = jt_source_extra = jt_sti_extra = nil first_key = second_key = as_extra = nil if through_reflection.options[:as] # has_many :through against a polymorphic join jt_foreign_key = through_reflection.options[:as].to_s + '_id' jt_as_extra = quot; AND %s.%s = %squot; % [ connection.quote_table_name(aliased_join_table_name), connection.quote_column_name(through_reflection.options[:as].to_s + '_type'), klass.quote_value(parent.active_record.base_class.name) ] else jt_foreign_key = through_reflection.primary_key_name
  43. 43. ] else jt_foreign_key = through_reflection.primary_key_name end case source_reflection.macro when :has_many if source_reflection.options[:as] first_key = quot;#{source_reflection.options[:as]}_idquot; second_key = options[:foreign_key] || primary_key as_extra = quot; AND %s.%s = %squot; % [ connection.quote_table_name(aliased_table_name), connection.quote_column_name(quot;#{source_reflection.options[:as]}_typequot;), klass.quote_value(source_reflection.active_record.base_class.name) ] else first_key = through_reflection.klass.base_class.to_s.foreign_key second_key = options[:foreign_key] || primary_key end unless through_reflection.klass.descends_from_active_record? jt_sti_extra = quot; AND %s.%s = %squot; % [ connection.quote_table_name(aliased_join_table_name), connection.quote_column_name(through_reflection.active_record.inheritance_col through_reflection.klass.quote_value(through_reflection.klass.sti_name)] end when :belongs_to first_key = primary_key if reflection.options[:source_type] second_key = source_reflection.association_foreign_key jt_source_extra = quot; AND %s.%s = %squot; % [ connection.quote_table_name(aliased_join_table_name), connection.quote_column_name(reflection.source_reflection.options[:foreign_ty klass.quote_value(reflection.options[:source_type]) ]
  44. 44. connection.quote_column_name(reflection.source_reflection.options[:foreign_ty klass.quote_value(reflection.options[:source_type]) ] else second_key = source_reflection.primary_key_name end end quot; #{join_type} %s ON (%s.%s = %s.%s%s%s%s) quot; % [ table_alias_for(through_reflection.klass.table_name, aliased_join_table_name), connection.quote_table_name(parent.aliased_table_name), connection.quote_column_name(parent.primary_key), connection.quote_table_name(aliased_join_table_name), connection.quote_column_name(jt_foreign_key), jt_as_extra, jt_source_extra, jt_sti_extra ]+ quot; #{join_type} %s ON (%s.%s = %s.%s%s) quot; % [ table_name_and_alias, connection.quote_table_name(aliased_table_name), connection.quote_column_name(first_key), connection.quote_table_name(aliased_join_table_name), connection.quote_column_name(second_key), as_extra ] when reflection.options[:as] && [:has_many, :has_one].include?(reflection.macro) quot; #{join_type} %s ON %s.%s = %s.%s AND %s.%s = %squot; % [ table_name_and_alias, connection.quote_table_name(aliased_table_name), quot;#{reflection.options[:as]}_idquot;, connection.quote_table_name(parent.aliased_table_name), parent.primary_key, connection.quote_table_name(aliased_table_name), quot;#{reflection.options[:as]}_typequot;, klass.quote_value(parent.active_record.base_class.name)
  45. 45. connection.quote_table_name(aliased_table_name), quot;#{reflection.options[:as]}_typequot;, klass.quote_value(parent.active_record.base_class.name) ] else foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key quot; #{join_type} %s ON %s.%s = %s.%s quot; % [ table_name_and_alias, aliased_table_name, foreign_key, parent.aliased_table_name, reflection.options[:primary_key] || parent.primary_key ] end when :belongs_to quot; #{join_type} %s ON %s.%s = %s.%s quot; % [ table_name_and_alias, connection.quote_table_name(aliased_table_name), reflection.klass.primary_key, connection.quote_table_name(parent.aliased_table_name), options[:foreign_key] || reflection.primary_key_name ] else quot;quot; end || '' join << %(AND %s) % [ klass.send(:type_condition, aliased_table_name)] unless klass.descends_from_active_record [through_reflection, reflection].each do |ref| join << quot;AND #{interpolate_sql(sanitize_sql(ref.options[:conditions], aliased_table_name) end join end
  46. 46. automation
  47. 47. lib/tasks/quality.rake require 'flog' desc quot;Analyze for code complexityquot; task :flog do flog = Flog.new flog.flog_files ['app'] threshold = 30 bad_methods = flog.totals.select do |name, score| score > threshold end bad_methods.sort { |a,b| a[1] <=> b[1] }.each do |name, score| puts quot;%8.1f: %squot; % [score, name] end raise quot;#{bad_methods.size} methods have a flog complexity > #{threshold}quot; unless bad_methods.empty? end
  48. 48. ~/Data/runway $ rake flog (in /Users/marty/Data/runway) 35.2: Token#tokenize 42.3: Token#parse_incubation_days 60.4: Token#parse_incubation_date 81.7: ActionParser#parse 89.7: ActionFormat#format rake aborted! 5 methods have a flog complexity > 30
  49. 49. use flog in your CI build
  50. 50. Flay http://ruby.sadi.st/Flay.html
  51. 51. Structural similarity checking of Sexp nodes (syntax tokens)
  52. 52. Fine grained duplication checking
  53. 53. Look for high scores (> 30)
  54. 54. $ sudo gem install flay $ flay [dir ...]
  55. 55. ~/Data/runway $ flay app [Processing files...] Total score (lower is better) = 286 1) Similar code found in :defn (mass = 60) app/controllers/signups_controller.rb:5 app/controllers/user_sessions_controller.rb:6 2) Similar code found in :if (mass = 54) app/controllers/forgot_passwords_controller.rb:15 app/controllers/signups_controller.rb:16 3) Similar code found in :defn (mass = 50) app/helpers/actions_helper.rb:43 app/helpers/actions_helper.rb:49 [More content removed...]
  56. 56. ~/Data/runway $ flay app [Processing files...] Total score (lower is better) = 286 1) Similar code found in :defn (mass = 60) app/controllers/signups_controller.rb:5 app/controllers/user_sessions_controller.rb:6 2) Similar code found in :if (mass = 54) app/controllers/forgot_passwords_controller.rb:15 app/controllers/signups_controller.rb:16 3) Similar code found in :defn (mass = 50) app/helpers/actions_helper.rb:43 app/helpers/actions_helper.rb:49 [More content removed...]
  57. 57. 1) Similar code found in :defn (mass = 60) app/controllers/signups_controller.rb:5 app/controllers/user_sessions_controller.rb:6 def create user = User.new(params[:signup]) if user.save head(:status => :created, :location => home_path) else head(:status => :unprocessable_entity) end end def create user_session = UserSession.new(params[:user_session]) if user_session.save head(:status => :created, :location => home_path) else head(:status => :unprocessable_entity) end end
  58. 58. What about Rails?
  59. 59. 1132 duplicates
  60. 60. actionmailer/install.rb require 'rbconfig' require 'find' require 'ftools' include Config # this was adapted from rdoc's install.rb by way of Log4r $sitedir = CONFIG[quot;sitelibdirquot;] unless $sitedir version = CONFIG[quot;MAJORquot;] + quot;.quot; + CONFIG[quot;MINORquot;] $libdir = File.join(CONFIG[quot;libdirquot;], quot;rubyquot;, version) $sitedir = $:.find {|x| x =~ /site_ruby/ } if !$sitedir $sitedir = File.join($libdir, quot;site_rubyquot;) elsif $sitedir !~ Regexp.quote(version) $sitedir = File.join($sitedir, version) end end # the actual gruntwork Dir.chdir(quot;libquot;) Find.find(quot;action_mailerquot;, quot;action_mailer.rbquot;) { |f| if f[-3..-1] == quot;.rbquot; File::install(f, File.join($sitedir, *f.split(///)), 0644, true) else File::makedirs(File.join($sitedir, *f.split(///))) end }
  61. 61. activesupport/install.rb require 'rbconfig' require 'find' require 'ftools' include Config # this was adapted from rdoc's install.rb by ways of Log4r $sitedir = CONFIG[quot;sitelibdirquot;] unless $sitedir version = CONFIG[quot;MAJORquot;] + quot;.quot; + CONFIG[quot;MINORquot;] $libdir = File.join(CONFIG[quot;libdirquot;], quot;rubyquot;, version) $sitedir = $:.find {|x| x =~ /site_ruby/ } if !$sitedir $sitedir = File.join($libdir, quot;site_rubyquot;) elsif $sitedir !~ Regexp.quote(version) $sitedir = File.join($sitedir, version) end end # the actual gruntwork Dir.chdir(quot;libquot;) Find.find(quot;active_supportquot;, quot;active_support.rbquot;) { |f| if f[-3..-1] == quot;.rbquot; File::install(f, File.join($sitedir, *f.split(///)), 0644, true) else File::makedirs(File.join($sitedir, *f.split(///))) end }
  62. 62. activerecord/install.rb require 'rbconfig' require 'find' require 'ftools' include Config # this was adapted from rdoc's install.rb by ways of Log4r $sitedir = CONFIG[quot;sitelibdirquot;] unless $sitedir version = CONFIG[quot;MAJORquot;] + quot;.quot; + CONFIG[quot;MINORquot;] $libdir = File.join(CONFIG[quot;libdirquot;], quot;rubyquot;, version) $sitedir = $:.find {|x| x =~ /site_ruby/ } if !$sitedir $sitedir = File.join($libdir, quot;site_rubyquot;) elsif $sitedir !~ Regexp.quote(version) $sitedir = File.join($sitedir, version) end end # the actual gruntwork Dir.chdir(quot;libquot;) Find.find(quot;active_recordquot;, quot;active_record.rbquot;) { |f| if f[-3..-1] == quot;.rbquot; File::install(f, File.join($sitedir, *f.split(///)), 0644, true) else File::makedirs(File.join($sitedir, *f.split(///))) end }
  63. 63. actionpack/install.rb require 'rbconfig' require 'find' require 'ftools' include Config # this was adapted from rdoc's install.rb by way of Log4r $sitedir = CONFIG[quot;sitelibdirquot;] unless $sitedir version = CONFIG[quot;MAJORquot;] + quot;.quot; + CONFIG[quot;MINORquot;] $libdir = File.join(CONFIG[quot;libdirquot;], quot;rubyquot;, version) $sitedir = $:.find {|x| x =~ /site_ruby/ } if !$sitedir $sitedir = File.join($libdir, quot;site_rubyquot;) elsif $sitedir !~ Regexp.quote(version) $sitedir = File.join($sitedir, version) end end # the actual gruntwork Dir.chdir(quot;libquot;) Find.find(quot;action_controllerquot;, quot;action_controller.rbquot;, quot;action_viewquot;, quot;action_view.rbquot;) { |f| if f[-3..-1] == quot;.rbquot; File::install(f, File.join($sitedir, *f.split(///)), 0644, true) else File::makedirs(File.join($sitedir, *f.split(///))) end }
  64. 64. automation
  65. 65. lib/tasks/quality.rake require 'flay' desc quot;Analyze for code duplicationquot; task :flay do threshold = 25 flay = Flay.new({:fuzzy => false, :verbose => false, :mass => threshold}) flay.process(*Flay.expand_dirs_to_files(['app'])) flay.report raise quot;#{flay.masses.size} chunks of code have a duplicate mass > #{threshold}quot; unless flay.masses.empty? end
  66. 66. ~/Data/runway $ rake flay (in /Users/marty/Data/runway) Total score (lower is better) = 164 1) Similar code found in :defn (mass = 60) app/controllers/signups_controller.rb:5 app/controllers/user_sessions_controller.rb:6 2) Similar code found in :if (mass = 54) app/controllers/forgot_passwords_controller.rb:15 app/controllers/signups_controller.rb:16 3) Similar code found in :defn (mass = 50) app/helpers/actions_helper.rb:43 app/helpers/actions_helper.rb:49 rake aborted! 3 chunks of code have a duplicate mass > 25
  67. 67. use flay in your CI build
  68. 68. roodi http://roodi.rubyforge.org/
  69. 69. Checks for design problems
  70. 70. Class Name Block Cyclomatic Complexity Method Name Method Cyclomatic Complexity Module Name Assignment In Conditional Class Line Count Case Missing Else Method Line Count Empty Rescue Body Module Line Count For Loop Check Parameter Number
  71. 71. Look for any errors
  72. 72. $ sudo gem install roodi $ roodi [pattern ...]
  73. 73. ~/Data/runway $ roodi quot;app/**/*.rbquot; app/models/action_format.rb:10 Method name quot;formatquot; has a cyclomatic complexity is 12. It should be 8 or less. app/models/action_parser.rb:50 Block cyclomatic complexity is 11. It should be 4 or less. app/models/action.rb:107 Case statement is missing an else clause. app/models/action.rb:103 Method quot;apply_defaults_from_namequot; has 23 lines. It should have 20 or less. app/controllers/application.rb:52 Found = in conditional. It should probably be an ==
  74. 74. Method name quot;formatquot; has a cyclomatic complexit It should be 8 or less. app/models/action_parser.rb:50 Block cyclomatic complexity is 11. It should be 4 or less. app/models/action.rb:107 Case statement is missing an else clause. app/models/action.rb:103 Method quot;apply_defaults_from_namequot; has 23 lines. It should have 20 or less. app/controllers/application.rb:52 Found = in conditional. It should probably be an ==
  75. 75. app/models/action_parser.rb:50 Block cyclomatic complexity is 11. It should be 4 or less. meta.each do |token| case token.klass when :context unless token.value.blank? @contexts ||= [] @contexts << token.value end when :time @time = token.value when :energy @energy = token.value when :tag @tags << token.value unless token.value.blank? when :semi_colon # Ignore when :available, :today, :later, :done @status = token.klass.to_s @effective_at = token.value when :due_at @due_at = token.value end end
  76. 76. app/models/action_parser.rb:50 Block cyclomatic complexity is 11. It should be 4 or less. 1 meta.each do |token| case token.klass 2 when :context 3 unless token.value.blank? @contexts ||= [] 4 @contexts << token.value end 5 when :time @time = token.value 6 when :energy @energy = token.value 7 8 when :tag @tags << token.value unless token.value.blank? 9 when :semi_colon # Ignore 10 when :available, :today, :later, :done @status = token.klass.to_s @effective_at = token.value 11 when :due_at @due_at = token.value end end
  77. 77. app/controllers/application.rb:52 Found = in conditional. It should probably be an == def extract_authenticity_token if token = request.headers[quot;HTTP_X_RUNWAY_AUTHENTICITY_TOKENquot;] params[request_forgery_protection_token] = token end end
  78. 78. What about Rails?
  79. 79. 1996 errors
  80. 80. ./actionpack/lib/action_controller/vendor/html- scanner/html/node.rb:417 Method name quot;matchquot; cyclomatic complexity is 47. It should be 8 or less.
  81. 81. def match(conditions) conditions = validate_conditions(conditions) # check content of child nodes if conditions[:content] if children.empty? return false unless match_condition(quot;quot;, conditions[:content]) else return false unless children.find { |child| child.match(conditions[:content]) } end end # test the name return false unless match_condition(@name, conditions[:tag]) if conditions[:tag] # test attributes (conditions[:attributes] || {}).each do |key, value| return false unless match_condition(self[key], value) end # test parent return false unless parent.match(conditions[:parent]) if conditions[:parent] # test children return false unless children.find { |child| child.match(conditions[:child]) } if conditio # test ancestors if conditions[:ancestor] return false unless catch :found do p = self throw :found, true if p.match(conditions[:ancestor]) while p = p.parent end end # test descendants
  82. 82. # test descendants if conditions[:descendant] return false unless children.find do |child| # test the child child.match(conditions[:descendant]) || # test the child's descendants child.match(:descendant => conditions[:descendant]) end end # count children if opts = conditions[:children] matches = children.select do |c| (c.kind_of?(HTML::Tag) and (c.closing == :self or ! c.childless?)) end matches = matches.select { |c| c.match(opts[:only]) } if opts[:only] opts.each do |key, value| next if key == :only case key when :count if Integer === value return false if matches.length != value else return false unless value.include?(matches.length) end when :less_than return false unless matches.length < value when :greater_than return false unless matches.length > value else raise quot;unknown count condition #{key}quot; end end end
  83. 83. else raise quot;unknown count condition #{key}quot; end end end # test siblings if conditions[:sibling] || conditions[:before] || conditions[:after] siblings = parent ? parent.children : [] self_index = siblings.index(self) if conditions[:sibling] return false unless siblings.detect do |s| s != self && s.match(conditions[:sibling]) end end if conditions[:before] return false unless siblings[self_index+1..-1].detect do |s| s != self && s.match(conditions[:before]) end end if conditions[:after] return false unless siblings[0,self_index].detect do |s| s != self && s.match(conditions[:after]) end end end true end
  84. 84. automation
  85. 85. lib/tasks/quality.rake require 'roodi' require 'roodi_task' RoodiTask.new 'roodi', ['app/**/*.rb', 'lib/**/*.rb'], 'roodi.yml'
  86. 86. roodi.yml # AssignmentInConditionalCheck: {} # CaseMissingElseCheck: {} ClassLineCountCheck: { line_count: 300 } ClassNameCheck: { pattern: !ruby/regexp /^[A-Z][a-zA-Z # ClassVariableCheck: {} CyclomaticComplexityBlockCheck: { complexity: 8 } CyclomaticComplexityMethodCheck: { complexity: 10 } EmptyRescueBodyCheck: { } ForLoopCheck: { } MethodLineCountCheck: { line_count: 20 } MethodNameCheck: { pattern: !ruby/regexp /^[_a-z<>=[|+ ModuleLineCountCheck: { line_count: 300 } ModuleNameCheck: { pattern: !ruby/regexp /^[A-Z][a-zA-Z ParameterNumberCheck: { parameter_count: 5 }
  87. 87. ~/Data/runway $ rake roodi (in /Users/marty/Data/runway) app/models/action_format.rb:10 - Method name quot;formatquot; cyclomatic complexity is 12. It should be 10 or less. app/models/action_parser.rb:11 - Method name quot;attributesquot; cyclomatic complexity is 12. It should be 10 or less. app/models/action_parser.rb:30 - Method name quot;parsequot; cyclomatic complexity is 14. It should be 10 or less. app/models/token.rb:173 - Method name quot;parse_incubation_datequot; cyclomatic complexity is 11. It should be 10 or less. app/models/token.rb:226 - Method name quot;parse_incubation_daysquot; cyclomatic complexity is 12. It should be 10 or less. app/models/action_parser.rb:50 - Block cyclomatic complexity is 11. It should be 8 or less. app/models/token.rb:11 - Block cyclomatic complexity is 10. It should be 8 or less. app/models/action.rb:103 - Method quot;apply_defaults_from_namequot; has 23 lines. It should have 20 or less. app/models/action_parser.rb:30 - Method quot;parsequot; has 23 lines. It should have 20 or less. rake aborted! Found 9 errors.
  88. 88. use roodi in your CI build
  89. 89. metric_fu http://metric-fu.rubyforge.org/
  90. 90. reports on everything you’ve seen today plus more!
  91. 91. $ sudo gem sources -a http://gems.github.com $ sudo gem install jscruggs-metric_fu
  92. 92. automation
  93. 93. lib/tasks/quality.rake require 'metric_fu'
  94. 94. $ rake metrics:all
  95. 95. use metrics_fu for regular human review
  96. 96. Summary
  97. 97. lib/tasks/quality.rake (part 1) require 'flog' desc quot;Analyze for code complexityquot; task :flog do flog = Flog.new flog.flog_files ['app'] threshold = 40 bad_methods = flog.totals.select do |name, score| score > threshold end bad_methods.sort { |a,b| a[1] <=> b[1] }.each do |name, score| puts quot;%8.1f: %squot; % [score, name] end raise quot;#{bad_methods.size} methods have a flog complexity > #{threshold}quot; unless bad_methods.empty? end
  98. 98. lib/tasks/quality.rake (part 2) require 'flay' desc quot;Analyze for code duplicationquot; task :flay do threshold = 25 flay = Flay.new({:fuzzy => false, :verbose => false, :mass => threshold}) flay.process(*Flay.expand_dirs_to_files(['app'])) flay.report raise quot;#{flay.masses.size} chunks of code have a duplicate mass > #{threshold}quot; unless flay.masses.empty? end
  99. 99. lib/tasks/quality.rake (part 3) require 'roodi' require 'roodi_task' require 'metric_fu' RoodiTask.new 'roodi', ['app/**/*.rb', 'lib/**/*.rb'], 'roodi.yml' task :quality => [:flog, :flay, :roodi, 'metrics:all']
  100. 100. continuous integration flog flay roodi regular human review metric_fu rake stats reek and more
  101. 101. Resources me @martinjandrews marty@cogentconsulting.com.au code samples from runwayapp.com reek wiki.github.com/kevinrutherford/reek flog ruby.sadi.st/Flog.html flay ruby.sadi.st/Flay.html roodi roodi.rubyforge.org metric_fu metric-fu.rubyforge.org

×