$30 off During Our Annual Pro Sale. View Details »

Deletion Driven Development: Code to delete code! (North American edition)

Chris Arcand
November 10, 2016

Deletion Driven Development: Code to delete code! (North American edition)

Good news! Ruby is a successful and mature programming language with a wealth of libraries and legacy applications that have been contributed to for many years. The bad news: Those projects might contain a large amount of useless, unused code which adds needless complexity and confuses new developers. In this talk I’ll explain how to build a static analysis tool to help you clear out the cruft - because there’s no code that’s easier to maintain than no code at all!

Chris Arcand

November 10, 2016
Tweet

More Decks by Chris Arcand

Other Decks in Programming

Transcript

  1. @chrisarcand Deletion Driven Development: Code to delete code!

  2. Chris Arcand
 chrisarcand chrisarcand www.chrisarcand.com

  3. None
  4. None
  5. “The Land of 10,000 Lakes”

  6. “The North Star State”

  7. “That super cold place.”

  8. “The Great White South, eh?”

  9. “That one place where those JRuby guys live, right?”

  10. MINNESOTA Minneapolis St. Paul

  11. None
  12. None
  13. None
  14. None
  15. None
  16. @chrisarcand chrisarcand.com I PROGRAMMING

  17. @chrisarcand chrisarcand.com I WRITING
 CODE

  18. @chrisarcand chrisarcand.com However…

  19. @chrisarcand chrisarcand.com I DELETING
 CODE

  20. @chrisarcand chrisarcand.com • Over-engineering
 Previous developer adding “potentially helpful” methods

    • Poorly written code
 Super-specific, single edge case methods • Refactoring, deprecations
 Callers get refactored over time and methods eventually aren’t needed anymore • etc, etc Dead code? How?
  21. @chrisarcand chrisarcand.com “Who cares?”

  22. @chrisarcand chrisarcand.com “Deleting Code” (2002) Ned Batchelder http://nedbatchelder.com/text/deleting-code.html

  23. @chrisarcand chrisarcand.com If you have a chunk of code you

    don't need any more, there's one big reason to delete it for real rather than leaving it in a disabled state: To reduce noise and uncertainty. Some of the worst enemies a developer has are noise or uncertainty in his code, because they prevent him from working with it effectively in the future. “
  24. @chrisarcand chrisarcand.com What if we could programmatically find unused code

    to delete?
  25. @chrisarcand chrisarcand.com Ruby code (.rb) ? ? ?

  26. @chrisarcand chrisarcand.com Part I: Parsing the code

  27. @chrisarcand chrisarcand.com Do you understand the following sequences of characters?

    How do you know? Modified examples and grammar from Robert B. Heckendorn, University of Idaho http://marvin.cs.uidaho.edu/Teaching/CS445/grammar.html
  28. @chrisarcand chrisarcand.com “The boy owns a dog.”

  29. @chrisarcand chrisarcand.com “A boy bites the dog.”

  30. @chrisarcand chrisarcand.com “Loves boy the.”

  31. @chrisarcand chrisarcand.com How could you programmatically determine which of those

    are correct… …and which are not?
  32. @chrisarcand chrisarcand.com ✨Context-free Grammar✨
 (CFG) A set of production rules

    that describe all possible strings in a given formal language. “What sentences are in the language and what are not?”
  33. @chrisarcand chrisarcand.com ✨Backus-Naur Form✨
 (BNF) One of two main notation

    techniques for context- free grammars, often used to describe computer programming languages.
  34. @chrisarcand chrisarcand.com <sentence> ::= <subject> <predicate> <subject> ::= <article> <noun>

    <predicate> ::= <verb> <direct-object> <direct-object> ::= <article> <noun> <article> ::= THE | A <noun> ::= BOY | DOG <verb> ::= OWNS | LOVES | BITES
  35. @chrisarcand chrisarcand.com <sentence> ::= <subject> <predicate> <subject> ::= <article> <noun>

    <predicate> ::= <verb> <direct-object> <direct-object> ::= <article> <noun> <article> ::= THE | A <noun> ::= BOY | DOG <verb> ::= OWNS | LOVES | BITES Symbols
  36. @chrisarcand chrisarcand.com <sentence> ::= <subject> <predicate> <subject> ::= <article> <noun>

    <predicate> ::= <verb> <direct-object> <direct-object> ::= <article> <noun> <article> ::= THE | A <noun> ::= BOY | DOG <verb> ::= OWNS | LOVES | BITES < non-terminals >
  37. @chrisarcand chrisarcand.com <sentence> ::= <subject> <predicate> <subject> ::= <article> <noun>

    <predicate> ::= <verb> <direct-object> <direct-object> ::= <article> <noun> <article> ::= THE | A <noun> ::= BOY | DOG <verb> ::= OWNS | LOVES | BITES TERMINALS
  38. @chrisarcand chrisarcand.com <sentence> ::= <subject> <predicate> <subject> ::= <article> <noun>

    <predicate> ::= <verb> <direct-object> <direct-object> ::= <article> <noun> <article> ::= THE | A <noun> ::= BOY | DOG <verb> ::= OWNS | LOVES | BITES Symbol on left is replaced with expression on the right ‘OR’ (choose between expressions) ‘definitional operator’
  39. @chrisarcand chrisarcand.com <sentence> ::= <subject> <predicate> <subject> ::= <article> <noun>

    <predicate> ::= <verb> <direct-object> <direct-object> ::= <article> <noun> <article> ::= THE | A <noun> ::= BOY | DOG <verb> ::= OWNS | LOVES | BITES The boy owns a dog.
  40. @chrisarcand chrisarcand.com <sentence> ::= <subject> <predicate> <subject> ::= <article> <noun>

    <predicate> ::= <verb> <direct-object> <direct-object> ::= <article> <noun> <article> ::= THE | A <noun> ::= BOY | DOG <verb> ::= OWNS | LOVES | BITES The boy owns a dog. <sentence>
  41. @chrisarcand chrisarcand.com <sentence> ::= <subject> <predicate> <subject> ::= <article> <noun>

    <predicate> ::= <verb> <direct-object> <direct-object> ::= <article> <noun> <article> ::= THE | A <noun> ::= BOY | DOG <verb> ::= OWNS | LOVES | BITES The boy owns a dog. <subject> <predicate>
  42. @chrisarcand chrisarcand.com <sentence> ::= <subject> <predicate> <subject> ::= <article> <noun>

    <predicate> ::= <verb> <direct-object> <direct-object> ::= <article> <noun> <article> ::= THE | A <noun> ::= BOY | DOG <verb> ::= OWNS | LOVES | BITES The boy owns a dog. <subject> <predicate>
  43. @chrisarcand chrisarcand.com <sentence> ::= <subject> <predicate> <subject> ::= <article> <noun>

    <predicate> ::= <verb> <direct-object> <direct-object> ::= <article> <noun> <article> ::= THE | A <noun> ::= BOY | DOG <verb> ::= OWNS | LOVES | BITES The boy owns a dog. <subject> <predicate> <article> <noun>
  44. @chrisarcand chrisarcand.com <sentence> ::= <subject> <predicate> <subject> ::= <article> <noun>

    <predicate> ::= <verb> <direct-object> <direct-object> ::= <article> <noun> <article> ::= THE | A <noun> ::= BOY | DOG <verb> ::= OWNS | LOVES | BITES The boy owns a dog. <subject> <predicate> <article> <noun>
  45. @chrisarcand chrisarcand.com <sentence> ::= <subject> <predicate> <subject> ::= <article> <noun>

    <predicate> ::= <verb> <direct-object> <direct-object> ::= <article> <noun> <article> ::= THE | A <noun> ::= BOY | DOG <verb> ::= OWNS | LOVES | BITES The boy owns a dog. <subject> <predicate> <article> <noun> Terminal found
  46. @chrisarcand chrisarcand.com <sentence> ::= <subject> <predicate> <subject> ::= <article> <noun>

    <predicate> ::= <verb> <direct-object> <direct-object> ::= <article> <noun> <article> ::= THE | A <noun> ::= BOY | DOG <verb> ::= OWNS | LOVES | BITES The boy owns a dog. <subject> <predicate> <article> <noun>
  47. @chrisarcand chrisarcand.com <sentence> ::= <subject> <predicate> <subject> ::= <article> <noun>

    <predicate> ::= <verb> <direct-object> <direct-object> ::= <article> <noun> <article> ::= THE | A <noun> ::= BOY | DOG <verb> ::= OWNS | LOVES | BITES The boy owns a dog. <subject> <predicate> <article> <noun> Terminal found
  48. @chrisarcand chrisarcand.com <sentence> ::= <subject> <predicate> <subject> ::= <article> <noun>

    <predicate> ::= <verb> <direct-object> <direct-object> ::= <article> <noun> <article> ::= THE | A <noun> ::= BOY | DOG <verb> ::= OWNS | LOVES | BITES The boy owns a dog. <subject> <predicate> <article> <noun> <verb> <direct-object> <article> <noun>
  49. @chrisarcand chrisarcand.com <sentence> ::= <subject> <predicate> <subject> ::= <article> <noun>

    <predicate> ::= <verb> <direct-object> <direct-object> ::= <article> <noun> <article> ::= THE | A <noun> ::= BOY | DOG <verb> ::= OWNS | LOVES | BITES The boy owns a dog. <subject> <predicate> <article> <noun> <verb> <direct-object> <article> <noun>
  50. @chrisarcand chrisarcand.com The boy owns a dog. <subject> <predicate> <article>

    <noun> <verb> <direct-object> <article> <noun> <sentence>
  51. @chrisarcand chrisarcand.com <subject> <predicate> <article> <noun> <verb> <direct-object> <article> <noun>

    <sentence> THE BOY OWNS A DOG Parsing was successful!
  52. @chrisarcand chrisarcand.com “A boy bites the dog.”

  53. @chrisarcand chrisarcand.com <subject> <predicate> <article> <noun> <verb> <direct-object> <article> <noun>

    <sentence> A BOY BITES THE DOG Parsing was successful! …but do boys often bite dogs?
  54. @chrisarcand chrisarcand.com Loves boy the. “ ”

  55. @chrisarcand chrisarcand.com <sentence> ::= <subject> <predicate> <subject> ::= <article> <noun>

    <predicate> ::= <verb> <direct-object> <direct-object> ::= <article> <noun> <article> ::= THE | A <noun> ::= BOY | DOG <verb> ::= OWNS | LOVES Loves boy the. <subject> ??? <article> ??? <noun> ???
  56. @chrisarcand chrisarcand.com “The boy owns a dog.”
 “A boy bites

    the dog.” “Loves boy the.” In programming terms… OK OK, but perhaps
 semantically incorrect
 (A bug!) SYNTAX ERROR
  57. @chrisarcand chrisarcand.com How does Ruby know the meaning of these

    characters? class Person def initialize(name) @name = name end def say_hello puts “Hi! My name is #{@name}!” end end chris = Person.new(“Chris”) chris.say_hello
  58. @chrisarcand chrisarcand.com Figure 1-13, Ruby Under a Microscope by Pat

    Shaughnessy
 Used with permission How Ruby tokenizes and parses code
  59. @chrisarcand chrisarcand.com Figure 1-13, Ruby Under a Microscope by Pat

    Shaughnessy
 Used with permission How Ruby tokenizes and parses code Ruby uses a LALR parser generator called Bison Look-Ahead, Left, Reversed Rightmost Derivation LALR(k): k refers to the amount of look-ahead
  60. @chrisarcand chrisarcand.com Figure 1-13, Ruby Under a Microscope by Pat

    Shaughnessy
 Used with permission How Ruby tokenizes and parses code Ruby uses a LALR parser generator called Bison Look-Ahead, Left, Reversed Rightmost Derivation LALR(k): k refers to the amount of look-ahead
  61. @chrisarcand chrisarcand.com Racc https://github.com/tenderlove/racc
 An LALR(1) parser generator How we

    will tokenize and parse code ruby_parser https://github.com/seattlerb/ruby_parser
 A Ruby parser, written in Ruby, using racc
  62. @chrisarcand chrisarcand.com class Person def greet(name) puts "Hello #{name}!" end

    end person = Person.new person.greet("RubyConf")
  63. @chrisarcand chrisarcand.com class Person def greet(name) puts "Hello #{name}!" end

    end person = Person.new person.greet("RubyConf") > parser = RubyParser.for_current_ruby > parser.parse(File.read(“example1.rb”)) 1 s(:block, 2 s(:class, :Person, nil, 3 s(:defn, :greet, s(:args, :name), 4 s(:call, nil, :puts, 
 5 s(:dstr, "Hello ", s(:evstr, s(:lvar, :name)), s(:str, "!"))))), 6 s(:lasgn, :person, s(:call, s(:const, :Person), :new)), 7 s(:call, s(:lvar, :person), :greet, s(:str, "RubyConf"))) example1.rb
  64. @chrisarcand chrisarcand.com 1 s(:block, 2 s(:class, :Person, nil, 3 s(:defn,

    :greet, s(:args, :name), 4 s(:call, nil, :puts, 
 5 s(:dstr, "Hello ", s(:evstr, … 6 s(:lasgn, :person, s(:call, … 7 s(:call, s(:lvar, :person), :greet, …
  65. @chrisarcand chrisarcand.com ruby_parser racc Ruby code (.rb) Ruby23Parser Parsing Generic

    processing File.read() s-expression (parse tree) ? ? ?
  66. @chrisarcand chrisarcand.com Part II: Processing the s-expression

  67. @chrisarcand chrisarcand.com class MinimalSexpProcessor

  68. @chrisarcand chrisarcand.com def initialize @processors ={} public_methods.each do |name| case

    name when /^process_(.*)/ then @processors[$1.to_sym] = name.to_sym end end end end class MinimalSexpProcessor
  69. @chrisarcand chrisarcand.com process_defn

  70. @chrisarcand chrisarcand.com process_defn

  71. @chrisarcand chrisarcand.com process_defn

  72. @chrisarcand chrisarcand.com defn =>:process_defn : @processors = { } Node

    type Corresponding processor
  73. @chrisarcand chrisarcand.com def process(exp) return nil if exp.nil? type =

    exp.first meth = @processors[type] || default_method return nil unless meth if meth == default_method && warn_on_default puts "WARNING: Using default method #{meth} for #{type}" end self.send(meth, exp) if meth end
  74. @chrisarcand chrisarcand.com class MinimalSexpProcessor attr_accessor :default_method attr_accessor :warn_on_default def initialize

    @default_method = nil @warn_on_default = true @processors ={} public_methods.each do |name| case name when /^process_(.*)/ then @processors[$1.to_sym] = name.to_sym end end end end def process(exp) return nil if exp.nil? type = exp.first meth = @processors[type] || default_method return nil unless meth if meth == default_method && warn_on_default puts "WARNING: Using default method #{meth} for #{type}" end self.send(meth, exp) if meth end
  75. @chrisarcand chrisarcand.com class MinimalSexpProcessor

  76. @chrisarcand chrisarcand.com class SillyProcessor < MinimalSexpProcessor

  77. @chrisarcand chrisarcand.com def process_defn(exp) method_name = exp[1] puts "PROCESSING A

    METHOD DEFINITION NODE: #{method_name}" process_until_empty(exp) s() end def process_not_defn(exp) node_type = exp[0] puts "Processing node: #{node_type}" process_until_empty(exp) s() end
  78. @chrisarcand chrisarcand.com def process_until_empty(exp) until exp.empty? sexp = exp.shift process(sexp)

    if Sexp === sexp end end
  79. @chrisarcand chrisarcand.com def initialize super @default_method = :process_not_defn @warn_on_default =

    false end
  80. @chrisarcand chrisarcand.com class SillyProcessor < MinimalSexpProcessor def process_defn(exp) method_name =

    exp[1] puts "PROCESSING A METHOD DEFINITION NODE: #{method_name}" process_until_empty(exp) s() end def process_not_defn(exp) node_type = exp[0] puts "Processing node: #{node_type}" process_until_empty(exp) s() end def process_until_empty(exp) until exp.empty? sexp = exp.shift process(sexp) if Sexp === sexp end end end def initialize super @default_method = :process_not_defn @warn_on_default = false end
  81. @chrisarcand chrisarcand.com 1 require 'ruby_parser' 2 require_relative 'minimal_sexp_processor.rb' 3 require_relative

    'silly_processor.rb' 4 5 parser = RubyParser.for_current_ruby 6 file = File.read(ARGV[0]) 7 sexp = parser.parse(file) 8 9 processor = SillyProcessor.new 10 processor.process(sexp) silly_processor_demo.rb
  82. @chrisarcand chrisarcand.com $ ruby minimal_parser_demo.rb example1.rb Processing node: block Processing

    node: class PROCESSING A METHOD DEFINITION NODE: greet Processing node: args Processing node: call Processing node: dstr Processing node: evstr Processing node: lvar Processing node: str Processing node: lasgn Processing node: call Processing node: const Processing node: call Processing node: lvar Processing node: str 1 class Person 2 def greet(name) 3 puts "Hello #{name}!" 4 end 5 end 6 7 person = Person.new 8 person.greet("RubyConf") example1.rb
  83. @chrisarcand chrisarcand.com class MethodTrackingProcessor < MinimalSexpProcessor

  84. @chrisarcand chrisarcand.com attr_reader :method_locations def initialize super @default_method = :process_until_empty

    @warn_on_default = false @class_stack = [] @method_stack = [] @method_locations = {} end class MethodTrackingProcessor < MinimalSexpProcessor
  85. @chrisarcand chrisarcand.com def process_defn(exp) exp.shift # node type name =

    exp.shift in_method(name, exp.file, exp.line) do process_until_empty(exp) end s() end def process_class(exp) exp.shift # node type in_klass(exp.shift) do process_until_empty(exp) end s() end def process_until_empty(exp) until exp.empty? sexp = exp.shift process(sexp) if Sexp === sexp end end class MethodTrackingProcessor < MinimalSexpProcessor attr_reader :method_locations def initialize super @default_method = :process_until_empty @warn_on_default = false @class_stack = [] @method_stack = [] @method_locations = {} end
  86. @chrisarcand chrisarcand.com def in_method(name, file, line) method_name = Regexp ===

    name ? name.inspect : name.to_s @method_stack.unshift(method_name) @method_locations[signature] = "#{file}:#{line}" yield ensure @method_stack.shift end def in_klass(name) @class_stack.unshift(name) with_new_method_stack do yield end ensure @class_stack.shift end def with_new_method_stack old_method_stack, @method_stack = @method_stack, [] yield ensure @method_stack = old_method_stack end class MethodTrackingProcessor < MinimalSexpProcessor attr_reader :method_locations def initialize super @default_method = :process_until_empty @warn_on_default = false @class_stack = [] @method_stack = [] @method_locations = {} end def process_defn(exp) exp.shift # node type name = exp.shift in_method(name, exp.file, exp.line) do process_until_empty(exp) end s() end def process_class(exp) exp.shift # node type in_klass(exp.shift) do process_until_empty(exp) end s() end def process_until_empty(exp) until exp.empty? sexp = exp.shift process(sexp) if Sexp === sexp end end
  87. @chrisarcand chrisarcand.com def klass_name @class_stack.first end def method_name @method_stack.first end

    def signature "#{klass_name}##{method_name}" end class MethodTrackingProcessor < MinimalSexpProcessor attr_reader :method_locations def initialize super @default_method = :process_until_empty @warn_on_default = false @class_stack = [] @method_stack = [] @method_locations = {} end def process_defn(exp) exp.shift # node type name = exp.shift in_method(name, exp.file, exp.line) do process_until_empty(exp) end s() end def process_class(exp) exp.shift # node type in_klass(exp.shift) do process_until_empty(exp) end s() end def process_until_empty(exp) until exp.empty? sexp = exp.shift process(sexp) if Sexp === sexp end end def in_method(name, file, line) method_name = Regexp === name ? name.inspect : name.to_s @method_stack.unshift(method_name) @method_locations[signature] = "#{file}:#{line}" yield ensure @method_stack.shift end def in_klass(name) @class_stack.unshift(name) with_new_method_stack do yield end ensure @class_stack.shift end def with_new_method_stack old_method_stack, @method_stack = @method_stack, [] yield ensure @method_stack = old_method_stack end
  88. @chrisarcand chrisarcand.com class MethodTrackingProcessor < MinimalSexpProcessor attr_reader :method_locations def initialize

    super @default_method = :process_until_empty @warn_on_default = false @class_stack = [] @method_stack = [] @method_locations = {} end def process_defn(exp) exp.shift # node type name = exp.shift in_method(name, exp.file, exp.line) do process_until_empty(exp) end s() end def process_class(exp) exp.shift # node type in_klass(exp.shift) do process_until_empty(exp) end s() end def process_until_empty(exp) until exp.empty? sexp = exp.shift process(sexp) if Sexp === sexp end end def in_method(name, file, line) method_name = Regexp === name ? name.inspect : name.to_s @method_stack.unshift(method_name) @method_locations[signature] = "#{file}:#{line}" yield ensure @method_stack.shift end def in_klass(name) @class_stack.unshift(name) with_new_method_stack do yield end ensure @class_stack.shift end def with_new_method_stack old_method_stack, @method_stack = @method_stack, [] yield ensure @method_stack = old_method_stack end def klass_name @class_stack.first end def method_name @method_stack.first end def signature "#{klass_name}##{method_name}" end end
  89. @chrisarcand chrisarcand.com 1 class Person 2 def greet(name) 3 puts

    "Hello #{name}!" 4 end 5 6 def say_goodbye(name) 7 puts "Goodbye, #{name}!" 8 end 9 end 10 11 class Dog 12 def bark! 13 puts "Woof!" 14 end 15 end 16 17 person = Person.new 18 person.greet("RubyConf") example2.rb
  90. @chrisarcand chrisarcand.com 5 path = ARGV[0] 6 file = File.read(path)

    7 sexp = RubyParser.for_current_ruby.process(file, path) 8 9 processor = MethodTrackingProcessor.new 10 processor.process(sexp) 11 pp processor.method_locations method_tracking_demo.rb $ ruby minimal_parser_demo.rb example2.rb { "Person#greet" => "example2.rb:2", "Person#say_goodbye" => "example2.rb:6", “Dog#bark!" => "example2.rb:12" }
  91. @chrisarcand chrisarcand.com ruby_parser racc Ruby code (.rb) Ruby23Parser MethodTrackingProcessor MinimalSexpProcessor

    Parsing Generic processing File.read() s-expression (parse tree) Call processing and reporting ? ? ?
  92. @chrisarcand chrisarcand.com Part III: Building the dead method finder
 (or,

    drawing the rest of the damn owl)
  93. @chrisarcand chrisarcand.com ruby_parser racc Ruby code (.rb) Ruby23Parser Parsing Generic

    processing File.read() s-expression (parse tree) Call processing and reporting Our tool DeadMethodFinder MethodTrackingProcessor MinimalSexpProcessor
  94. @chrisarcand chrisarcand.com class DeadMethodFinder < MethodTrackingProcessor

  95. @chrisarcand chrisarcand.com attr_reader :known attr_reader :called def initialize super @known

    = Hash.new { |h,k| h[k] = Set.new } @called = Set.new end class DeadMethodFinder < MethodTrackingProcessor
  96. @chrisarcand chrisarcand.com def process_defn(exp) super do known[plain_method_name] << klass_name process_until_empty(exp)

    end end def process_call(exp) method_name = exp[2] called << method_name process_until_empty(exp) exp end attr_reader :known attr_reader :called def initialize super @known = Hash.new { |h,k| h[k] = Set.new } @called = Set.new end class DeadMethodFinder < MethodTrackingProcessor
  97. @chrisarcand chrisarcand.com def uncalled not_called = known.keys - called.to_a by_class

    = Hash.new { |h,k| h[k] = [] } not_called.each do |meth| known[meth].each do |klass| by_class[klass] << meth end end by_class end # #method_name -> :method_name def plain_method_name method_name.to_s.sub(/^::|#/, "").to_sym end attr_reader :known attr_reader :called def initialize super @known = Hash.new { |h,k| h[k] = Set.new } @called = Set.new end def process_defn(exp) super do known[plain_method_name] << klass_name process_until_empty(exp) end end def process_call(exp) method_name = exp[2] called << method_name process_until_empty(exp) exp end class DeadMethodFinder < MethodTrackingProcessor
  98. @chrisarcand chrisarcand.com attr_reader :known attr_reader :called def initialize super @known

    = Hash.new { |h,k| h[k] = Set.new } @called = Set.new end def process_defn(exp) super do known[plain_method_name] << klass_name process_until_empty(exp) end end def process_call(exp) method_name = exp[2] called << method_name process_until_empty(exp) exp end def uncalled not_called = known.keys - called.to_a by_class = Hash.new { |h,k| h[k] = [] } not_called.each do |meth| known[meth].each do |klass| by_class[klass] << meth end end by_class end # #method_name -> :method_name def plain_method_name method_name.to_s.sub(/^::|#/, "").to_sym end end class DeadMethodFinder < MethodTrackingProcessor
  99. @chrisarcand chrisarcand.com 1 class Person 2 def greet(name) 3 speak

    "Hello #{name}!" 4 end 5 6 # Never used 7 def say_goodbye(name) 8 speak "Goodbye, #{name}!" 9 end 10 11 def speak(text) 12 puts text 13 end 14 15 def pet_dog(dog) 16 dog.send(:pet) 17 end 18 end 19 20 class Dog 21 attr_accessor :fed # Never used 22 23 def bark! 24 puts "Bark!" 25 end 26 27 def pet 28 puts "Ahhh..." 29 end 30 end 31 32 33 chris = Person.new 34 reuben = Dog.new 35 36 chris.greet("RubyConf") 37 reuben.bark! 38 chris.pet_dog(reuben) example3.rb => Hello RubyConf!
 Bark! 
 Ahhh…
  100. @chrisarcand chrisarcand.com 1 class Person 2 def greet(name) 3 speak

    "Hello #{name}!" 4 end 5 6 # Never used 7 def say_goodbye(name) 8 speak "Goodbye, #{name}!" 9 end 10 11 def speak(text) 12 puts text 13 end 14 15 def pet_dog(dog) 16 dog.send(:pet) 17 end 18 end 19 20 class Dog 21 attr_accessor :fed # Never used 22 23 def bark! 24 puts "Bark!" 25 end 26 27 def pet 28 puts "Ahhh..." 29 end 30 end 31 32 33 chris = Person.new 34 reuben = Dog.new 35 36 chris.greet("RubyConf") 37 reuben.bark! 38 chris.pet_dog(reuben) example3.rb > processor = DeadMethodFinder.new > processor.process(sexp) > puts processor.uncalled => {"Person"=>[:say_goodbye], "Dog"=>[:pet]}
  101. @chrisarcand chrisarcand.com s(:call, s(:lvar, :dog), :send, s(:lit, :pet)) dog.send(:pet)

  102. @chrisarcand chrisarcand.com def process_call(exp) method_name = exp[2] case method_name when

    :send, :public_send, :__send__ msg_arg = exp[3] if Sexp === msg_arg && [:lit, :str].include?(msg_arg.sexp_type) called << msg_arg.last.to_sym end end called << method_name process_until_empty(exp) exp end s(:call, s(:lvar, :dog), :send, s(:lit, :pet)) 0 1 2 3
  103. @chrisarcand chrisarcand.com 1 class Person 2 def greet(name) 3 speak

    "Hello #{name}!" 4 end 5 6 # Never used 7 def say_goodbye(name) 8 speak "Goodbye, #{name}!" 9 end 10 11 def speak(text) 12 puts text 13 end 14 15 def pet_dog(dog) 16 dog.send(:pet) 17 end 18 end 19 20 class Dog 21 attr_accessor :fed # Never used 22 23 def bark! 24 puts "Bark!" 25 end 26 27 def pet 28 puts "Ahhh..." 29 end 30 end 31 32 33 chris = Person.new 34 reuben = Dog.new 35 36 chris.greet("RubyConf") 37 reuben.bark! 38 chris.pet_dog(reuben) example3.rb > processor = DeadMethodFinder.new > processor.process(sexp) > puts processor.uncalled => {"Person"=>[:say_goodbye]}
  104. @chrisarcand chrisarcand.com s(:call, nil, :attr_accessor, s(:lit, :fed)) attr_accessor :fed

  105. @chrisarcand chrisarcand.com s(:call, nil, :attr_accessor, s(:lit, :fed)) def process_call(exp) method_name

    = exp[2] case method_name when :attr_accessor _, _, _, *args = exp file, line = exp.file, exp.line args.each do |(_, name)| record_known_method(name, file, line) record_known_method("#{name}=".to_sym, file, line) end when :send, :public_send, :__send__ _, _, _, msg_arg, * = exp if Sexp === msg_arg && [:lit, :str].include?(msg_arg.sexp_type) called << msg_arg.last.to_sym end end called << method_name process_until_empty(exp) exp end def record_known_method(name, file, line) signature = "#{klass_name}##{name}" method_locations[signature] = "#{file}:#{line}" known[name] << klass_name end 0 1 2 3
  106. @chrisarcand chrisarcand.com def report puts "These methods MIGHT not be

    called:" uncalled.each do |klass, methods| not_called_methods = methods.map do |method| location = method_locations["#{klass}##{method}"] " %-35s %s" % [method, location] end not_called_methods.compact! next if not_called_methods.empty? puts "\n#{klass}" puts not_called_methods.join "\n" end end
  107. @chrisarcand chrisarcand.com > processor = DeadMethodFinder.new > processor.process(sexp)
 > processor.report

  108. @chrisarcand chrisarcand.com 1 class Person 2 def greet(name) 3 speak

    "Hello #{name}!" 4 end 5 6 # Never used 7 def say_goodbye(name) 8 speak "Goodbye, #{name}!" 9 end 10 11 def speak(text) 12 puts text 13 end 14 15 def pet_dog(dog) 16 dog.send(:pet) 17 end 18 end 19 20 class Dog 21 attr_accessor :fed # Never used 22 23 def bark! 24 puts "Bark!" 25 end 26 27 def pet 28 puts "Ahhh..." 29 end 30 end 31 32 33 chris = Person.new 34 reuben = Dog.new 35 36 chris.greet("RubyConf") 37 reuben.bark! 38 chris.pet_dog(reuben) example2.rb These methods MIGHT not be called:
 
 Person say_goodbye example2.rb:7 Dog fed example2.rb:21 fed= example2.rb:21
  109. @chrisarcand chrisarcand.com 1 class Person 2 def greet(name) 3 speak

    "Hello #{name}!" 4 end 5 6 7 8 9 10 11 def speak(text) 12 puts text 13 end 14 15 def pet_dog(dog) 16 dog.send(:pet) 17 end 18 end 19 20 class Dog 21 22 23 def bark! 24 puts "Bark!" 25 end 26 27 def pet 28 puts "Ahhh..." 29 end 30 end 31 32 33 chris = Person.new 34 reuben = Dog.new 35 36 chris.greet("RubyConf") 37 reuben.bark! 38 chris.pet_dog(reuben) # Never used def say_goodbye(name) speak "Goodbye, #{name}!" end attr_accessor :fed # Never used - - - - - example2.rb
  110. @chrisarcand chrisarcand.com ✌ Done?

  111. @chrisarcand chrisarcand.com Done Ruby is complex (to parse)

  112. @chrisarcand chrisarcand.com Done? Ruby is complex (to parse) …but adding

    edge cases is easy.
  113. @chrisarcand chrisarcand.com reuben.fed = true

  114. @chrisarcand chrisarcand.com 1 class Person 2 def greet(name) 3 speak

    "Hello #{name}!" 4 end 5 6 # Never used 7 def say_goodbye(name) 8 speak "Goodbye, #{name}!" 9 end 10 11 def speak(text) 12 puts text 13 end 14 15 def pet_dog(dog) 16 dog.send(:pet) 17 end 18 end 19 20 class Dog 21 attr_accessor :fed # Never used 22 23 def bark! 24 puts "Bark!" 25 end 26 27 def pet 28 puts "Ahhh..." 29 end 30 end 31 32 33 chris = Person.new 34 reuben = Dog.new 35 36 chris.greet("RubyConf") 37 reuben.bark! 38 chris.pet_dog(reuben)
 39 example4.rb Person say_goodbye example2.rb:7 Dog fed example2.rb:21 fed= example2.rb:21 reuben.fed = true
  115. Brought to you by: Julian Cheal

  116. @chrisarcand chrisarcand.com s(:attrasgn, s(:lvar, :reuben), :fed=, s(:true)) def process_attrasgn(exp) method_name

    = exp[2] method_name = method_name.last if Sexp === method_name called << method_name process_until_empty exp exp end reuben.fed = true
  117. @chrisarcand chrisarcand.com 1 class Person 2 def greet(name) 3 speak

    "Hello #{name}!" 4 end 5 6 # Never used 7 def say_goodbye(name) 8 speak "Goodbye, #{name}!" 9 end 10 11 def speak(text) 12 puts text 13 end 14 15 def pet_dog(dog) 16 dog.send(:pet) 17 end 18 end 19 20 class Dog 21 attr_accessor :fed # Never used 22 23 def bark! 24 puts "Bark!" 25 end 26 27 def pet 28 puts "Ahhh..." 29 end 30 end 31 32 33 chris = Person.new 34 reuben = Dog.new 35 36 chris.greet("RubyConf") 37 reuben.bark! 38 chris.pet_dog(reuben)
 39 reuben.fed = true example4.rb Person say_goodbye example2.rb:7 Dog fed example2.rb:21
  118. @chrisarcand chrisarcand.com What about Rails DSL?

  119. @chrisarcand chrisarcand.com :after_commit :before_create :after_create :before_destroy :before_filter :before_action :after_validation :before_update

    :around_save :validates :validates_length_of :validates_format_of :validates_cuteness_of :validates_confirmation_of :validate
  120. @chrisarcand chrisarcand.com :after_commit :before_create :after_create :before_destroy :before_filter :before_action :after_validation :before_update

    :around_save :validates :validates_length_of :validates_format_of :validates_cuteness_of :validates_confirmation_of :validate
  121. @chrisarcand chrisarcand.com What about my own DSL?

  122. @chrisarcand chrisarcand.com

  123. @chrisarcand chrisarcand.com class Disk < ActiveRecord::Base has_many :partitions virtual_column :allocated_space,

    type: :integer, uses: :partitions virtual_column :used_percent_of_provisioned, type: :float virtual_has_many :storage_systems, class_name: "CimComputerSystem" def allocated_space # ... end def used_percent_of_provisioned # ... end end
  124. @chrisarcand chrisarcand.com RAILS_DSL_METHODS = [ :after_action, :around_action, :before_action, ...
 ...

    ] def process_call(exp) # ... when *RAILS_DSL_METHODS + AR_VIRTUAL_DSL_METHODS _, _, _, (_, new_name), possible_hash = exp called << new_name if Sexp === possible_hash && possible_hash.sexp_type == :hash possible_hash.sexp_body.each_slice(2) do |key, val| next unless Sexp === val called << val.last if val.first == :lit called << val.last.to_sym if val.first == :str end end # ... end
  125. @chrisarcand chrisarcand.com As with most things, with the right tools,

    the job isn’t very difficult. Customization is easy.
  126. @chrisarcand chrisarcand.com Other things that are easy: Executing this code

    on your project, right now.
  127. @chrisarcand chrisarcand.com debride https://github.com/seattlerb/debride debride (v): To remove dead, contaminated,

    or adherent tissue and/or foreign material.
  128. @chrisarcand chrisarcand.com ruby_parser racc Ruby code (.rb) Ruby23Parser Parsing File.read()

  129. @chrisarcand chrisarcand.com ruby_parser racc sexp_processor Ruby code (.rb) Ruby23Parser MethodBasedSexpProcessor

    SexpProcessor Parsing Generic processing File.read() s-expression (parse tree)
  130. @chrisarcand chrisarcand.com ruby_parser racc sexp_processor Ruby code (.rb) Ruby23Parser MethodBasedSexpProcessor

    SexpProcessor Parsing Generic processing File.read() s-expression (parse tree) Call processing and reporting debride Debride
  131. @chrisarcand chrisarcand.com $ debride [options] files_or_dirs -e, --exclude FILE1,FILE2,ETC -w,

    --whitelist PATTERN -f, --focus PATH -r, --rails -v, --verbose
  132. @chrisarcand chrisarcand.com - @called = Set.new + @called = Hash.new

    { |h,k| h[k] = 0 } - called << method_name + called[method_name] += 1 - not_called = known.keys - called.to_a + called_once = called.select { |_, v| v == 1 } API Quality Control - Single call methods
  133. @chrisarcand chrisarcand.com Future considerations: • Pushing more work upstream ✴

    Bug fixes ✴ Cleanup ✴ Potential features
  134. @chrisarcand chrisarcand.com But wait, there’s more! Unused by Josh Clayton


    https://github.com/joshuaclayton/unused
 Written in Haskell, this project utilizes ctags to statically find unused code OldeCodeFinder by Tom Copeland
 https://github.com/tcopeland/olde_code_finder
 A Ruby gem checking code content by date and authorship Here’s a few…
  135. @chrisarcand chrisarcand.com No code is faster than no code. No

    code has fewer bugs than no code. No code is easier to understand than no code. No code is more maintainable than no code.
  136. Thank you!
 Questions? chrisarcand chrisarcand www.chrisarcand.com