Code Quality Analysis

13,260 views
13,107 views

Published on

Published in: Technology
0 Comments
35 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
13,260
On SlideShare
0
From Embeds
0
Number of Embeds
6,260
Actions
Shares
0
Downloads
479
Comments
0
Likes
35
Embeds 0
No embeds

No notes for slide














































































































  • 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

    ×