Code Quality Analysis
       Marty Andrews
      @martinjandrews

                        .com.au
static analysis
                  =
software analysis without execution
no app runtime required!
feedback!
design improvement


improve testability


              manage complexity
Whole
                               Team




           Collective                            Coding
           Ownership...
Collective                                       Coding
Ownership                                       Standard
         ...
smell the
  code
“A code smell is a surface indication
that usually corresponds to a deeper
       problem in the system.”

         -- Mar...
“If it stinks, change it.”

     -- Grandma Beck
Tools
code samples from


 runwayapp.com
rake stats
Summary statistics from your Rails app
Look for a good code to test ratio
$   sudo gem install rails
$   rails my_app
$   cd my_app
$   rake stats
~/Data/runway $ rake stats
(in /Users/marty/Data/runway)
+----------------------+-------+-------+---------+---------+-----...
681 |       12 |      96 |   8|     5
 750 |       24 |     113 |   4|     4
1395 |        0|        0|    0|     0
 434 |...
use rake stats for regular
     human review
reek
http://wiki.github.com/kevinrutherford/reek
Searches for code smells
Control Couple   Long Parameter List
Duplication      Nested Iterators
Feature Envy     Uncommunicative
                 N...
Look for warnings that you haven’t
         thought about
$ sudo gem install reek
$ reek [file ...]
~/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]...
[Feature Envy] Element#update_from refers to
response more than self




  def parse_incubation_day_of_the_month
    if @r...
What about Rails?
10,645 warnings
use reek for regular human
          review
Flog
http://ruby.sadi.st/Flog.html
Complexity checking of Ruby code
“ABC” like algorithm

    Assignments
     Branches
    Conditionals
Look for high scores (> 40)
$ sudo gem install flog
$ flog [dir ...]
~/Data/runway $ flog app

  1426.9: flog total
     7.9: flog/method average

    89.7:   ActionFormat#format
    81.7:   ...
~/Data/runway $ flog app

  1426.9: flog total
     7.9: flog/method average

    89.7:   ActionFormat#format
    81.7:   ...
89.7: ActionFormat#format


def format
  @tokens = []
  @semi = false

  append(quot;Waiting forquot;) if @action.waiting?...
What about Rails?
~/Data/rails $ flog .

201079.7: flog total
    14.1: flog/method average

  3841.5:   main#none
   866.1:   Parser#none
 ...
841.5:   main#none
866.1:   Parser#none
709.7:   timezone#Europe/London
707.5:   timezone#America/St_Johns
695.0:   timezo...
def association_join
  connection = reflection.active_record.connection
  join = case reflection.macro
    when :has_and_b...
]
else
  jt_foreign_key = through_reflection.primary_key_name
end

case source_reflection.macro
when :has_many
  if source...
connection.quote_column_name(reflection.source_reflection.options[:foreign_ty
        klass.quote_value(reflection.options...
connection.quote_table_name(aliased_table_name),
            quot;#{reflection.options[:as]}_typequot;,
            klass....
automation
lib/tasks/quality.rake


require 'flog'

desc quot;Analyze for code complexityquot;
task :flog do
  flog = Flog.new
  flog...
~/Data/runway $ rake flog
(in /Users/marty/Data/runway)
    35.2: Token#tokenize
    42.3: Token#parse_incubation_days
   ...
use flog in your CI build
Flay
http://ruby.sadi.st/Flay.html
Structural similarity checking of Sexp
       nodes (syntax tokens)
Fine grained duplication checking
Look for high scores (> 30)
$ sudo gem install flay
$ flay [dir ...]
~/Data/runway $ flay app

[Processing files...]

Total score (lower is better) = 286


1) Similar code found in :defn (mas...
~/Data/runway $ flay app

[Processing files...]

Total score (lower is better) = 286


1) Similar code found in :defn (mas...
1) Similar code found in :defn (mass = 60)
  app/controllers/signups_controller.rb:5
  app/controllers/user_sessions_contr...
What about Rails?
1132 duplicates
actionmailer/install.rb
require 'rbconfig'
require 'find'
require 'ftools'

include Config

# this was adapted from rdoc's...
activesupport/install.rb
require 'rbconfig'
require 'find'
require 'ftools'

include Config

# this was adapted from rdoc'...
activerecord/install.rb
require 'rbconfig'
require 'find'
require 'ftools'

include Config

# this was adapted from rdoc's...
actionpack/install.rb
require 'rbconfig'
require 'find'
require 'ftools'

include Config

# this was adapted from rdoc's i...
automation
lib/tasks/quality.rake


require 'flay'

desc quot;Analyze for code duplicationquot;
task :flay do
  threshold = 25
  flay...
~/Data/runway $ rake flay
(in /Users/marty/Data/runway)
Total score (lower is better) = 164

1) Similar code found in :def...
use flay in your CI build
roodi
http://roodi.rubyforge.org/
Checks for design problems
Class Name          Block Cyclomatic Complexity

Method Name         Method Cyclomatic
                    Complexity
Modu...
Look for any errors
$ sudo gem install roodi
$ roodi [pattern ...]
~/Data/runway $ roodi quot;app/**/*.rbquot;

app/models/action_format.rb:10

 Method name quot;formatquot; has a cyclomati...
Method name quot;formatquot; has a cyclomatic complexit

   It should be 8 or less.

app/models/action_parser.rb:50

 Bloc...
app/models/action_parser.rb:50

 Block cyclomatic complexity is 11.

 It should be 4 or less.

   meta.each do |token|
   ...
app/models/action_parser.rb:50

 Block cyclomatic complexity is 11.

 It should be 4 or less.

1        meta.each do |toke...
app/controllers/application.rb:52

 Found = in conditional.

 It should probably be an ==




  def extract_authenticity_t...
What about Rails?
1996 errors
./actionpack/lib/action_controller/vendor/html-
scanner/html/node.rb:417
  Method name quot;matchquot;
  cyclomatic comple...
def match(conditions)
  conditions = validate_conditions(conditions)
  # check content of child nodes
  if conditions[:con...
# test descendants
if conditions[:descendant]
  return false unless children.find do |child|
    # test the child
    chil...
else raise quot;unknown count condition #{key}quot;
      end
    end
  end

  # test siblings
  if conditions[:sibling] |...
automation
lib/tasks/quality.rake




require 'roodi'
require 'roodi_task'

RoodiTask.new 'roodi', ['app/**/*.rb', 'lib/**/*.rb'], 'r...
roodi.yml

# AssignmentInConditionalCheck:        {}
# CaseMissingElseCheck:                {}
ClassLineCountCheck:       ...
~/Data/runway $ rake roodi
(in /Users/marty/Data/runway)
app/models/action_format.rb:10 - Method name quot;formatquot; cyc...
use roodi in your CI build
metric_fu
 http://metric-fu.rubyforge.org/
reports on everything you’ve
         seen today

        plus more!
$ sudo gem sources -a http://gems.github.com
$ sudo gem install jscruggs-metric_fu
automation
lib/tasks/quality.rake




require 'metric_fu'
$ rake metrics:all
use metrics_fu for regular
     human review
Summary
lib/tasks/quality.rake (part 1)


require 'flog'

desc quot;Analyze for code complexityquot;
task :flog do
  flog = Flog.n...
lib/tasks/quality.rake (part 2)


require 'flay'

desc quot;Analyze for code duplicationquot;
task :flay do
  threshold = ...
lib/tasks/quality.rake (part 3)




require 'roodi'
require 'roodi_task'
require 'metric_fu'

RoodiTask.new 'roodi', ['app...
continuous integration
      flog         flay       roodi


   regular human review
             metric_fu

rake stats     ...
Resources
me
 
 
 
 
 
 @martinjandrews
  

 
 
 
 
 
 
 marty@cogentconsulting.com.au

code samples from
                ...
Code Quality Analysis
Code Quality Analysis
Code Quality Analysis
Code Quality Analysis
Code Quality Analysis
Code Quality Analysis
Code Quality Analysis
Code Quality Analysis
Code Quality Analysis
Upcoming SlideShare
Loading in...5
×

Code Quality Analysis

12,837

Published on

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

No Downloads
Views
Total Views
12,837
On Slideshare
0
From Embeds
0
Number of Embeds
8
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
    1. Gostou de algum slide específico?

      Recortar slides é uma maneira fácil de colecionar informações para acessar mais tarde.

    ×