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

956593b947dfddf6d89168fe09aa628a?s=47 Chris Arcand
September 10, 2016

Deletion Driven Development: Code to delete code! (RubyKaigi 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!

956593b947dfddf6d89168fe09aa628a?s=128

Chris Arcand

September 10, 2016
Tweet

Transcript

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

  2. Chris Arcand
 chrisarcand chrisarcand www.chrisarcand.com

  3. MINNESOTA

  4. MINNESOTA

  5. MINNESOTA Minneapolis St. Paul

  6. None
  7. None
  8. None
  9. None
  10. None
  11. @chrisarcand chrisarcand.com I PROGRAMMING

  12. @chrisarcand chrisarcand.com I WRITING
 CODE

  13. @chrisarcand chrisarcand.com However…

  14. @chrisarcand chrisarcand.com I DELETING
 CODE

  15. @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?
  16. @chrisarcand chrisarcand.com “Who cares?”

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

  18. @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. “
  19. @chrisarcand chrisarcand.com What if we could programmatically find unused code

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

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

  22. @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
  23. @chrisarcand chrisarcand.com “The boy owns a dog.”

  24. @chrisarcand chrisarcand.com “The dog loves the boy.”

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

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

    are correct… …and which are not?
  27. @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?”
  28. @chrisarcand chrisarcand.com ✨Backus-Naur Form✨
 (BNF) One of two main notation

    techniques for context- free grammars, often used to describe computer programming languages.
  29. @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
  30. @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
  31. @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 >
  32. @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
  33. @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’
  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 The boy owns a dog.
  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 The boy owns a dog. <sentence>
  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 The boy owns a dog. <subject> <predicate>
  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 The boy owns a dog. <subject> <predicate>
  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 The boy owns a dog. <subject> <predicate> <article> <noun>
  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. <subject> <predicate> <article> <noun>
  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. <subject> <predicate> <article> <noun> Terminal found
  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> <article> <noun>
  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> <article> <noun> Terminal found
  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> <verb> <direct-object> <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> <verb> <direct-object> <article> <noun>
  45. @chrisarcand chrisarcand.com The boy owns a dog. <subject> <predicate> <article>

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

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

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

    <sentence> THE DOG LOVES THE BOY Parsing was successful!
  49. @chrisarcand chrisarcand.com Loves boy the. “ ”

  50. @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> ???
  51. @chrisarcand chrisarcand.com “The boy owns a dog.”
 “The dog loves

    the boy.” “Loves boy the.” In programming terms… OK OK SYNTAX ERROR
  52. @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
  53. @chrisarcand chrisarcand.com Figure 1-13, Ruby Under a Microscope by Pat

    Shaughnessy
 Used with permission How Ruby tokenizes and parses code
  54. @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
  55. @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
  56. @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
  57. @chrisarcand chrisarcand.com class Person def greet(name) puts "Hello #{name}!" end

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

    end person = Person.new person.greet("RubyKaigi") > 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, "RubyKaigi"))) example1.rb
  59. @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, …
  60. @chrisarcand chrisarcand.com ruby_parser racc Ruby code (.rb) Ruby23Parser Parsing Generic

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

  62. @chrisarcand chrisarcand.com class MinimalSexpProcessor

  63. @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
  64. @chrisarcand chrisarcand.com process_defn

  65. @chrisarcand chrisarcand.com process_defn

  66. @chrisarcand chrisarcand.com process_defn

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

    type Corresponding processor
  68. @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
  69. @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
  70. @chrisarcand chrisarcand.com class MinimalSexpProcessor

  71. @chrisarcand chrisarcand.com class SillyProcessor < MinimalSexpProcessor

  72. @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
  73. @chrisarcand chrisarcand.com def process_until_empty(exp) until exp.empty? sexp = exp.shift process(sexp)

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

    false end
  75. @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
  76. @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
  77. @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("RubyKaigi") example1.rb
  78. @chrisarcand chrisarcand.com class MethodTrackingProcessor < MinimalSexpProcessor

  79. @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
  80. @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
  81. @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
  82. @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
  83. @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
  84. @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("RubyKaigi") example2.rb
  85. @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" }
  86. @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 ? ? ?
  87. @chrisarcand chrisarcand.com Part III: Building the dead method finder
 (or,

    drawing the rest of the damn owl)
  88. @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
  89. @chrisarcand chrisarcand.com class DeadMethodFinder < MethodTrackingProcessor

  90. @chrisarcand chrisarcand.com class DeadMethodFinder < MethodTrackingProcessor attr_reader :known attr_reader :called

    def initialize super @known = Hash.new { |h,k| h[k] = Set.new } @called = Set.new end
  91. @chrisarcand chrisarcand.com class DeadMethodFinder < MethodTrackingProcessor 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
  92. @chrisarcand chrisarcand.com class DeadMethodFinder < MethodTrackingProcessor 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
  93. @chrisarcand chrisarcand.com class DeadMethodFinder < MethodTrackingProcessor 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
  94. @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("RubyKaigi") 37 reuben.bark! 38 chris.pet_dog(reuben) example3.rb => Hello RubyKaigi!
 Bark! 
 Ahhh…
  95. @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("RubyKaigi") 37 reuben.bark! 38 chris.pet_dog(reuben) example3.rb > processor = DeadMethodFinder.new > processor.process(sexp) > puts processor.uncalled => {"Person"=>[:say_goodbye], "Dog"=>[:pet]}
  96. @chrisarcand chrisarcand.com s(:call, s(:lvar, :dog), :send, s(:lit, :pet)) dog.send(:pet)

  97. @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
  98. @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("RubyKaigi") 37 reuben.bark! 38 chris.pet_dog(reuben) example3.rb > processor = DeadMethodFinder.new > processor.process(sexp) > puts processor.uncalled => {"Person"=>[:say_goodbye]}
  99. @chrisarcand chrisarcand.com s(:call, nil, :attr_accessor, s(:lit, :fed)) attr_accessor :fed

  100. @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
  101. @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
  102. @chrisarcand chrisarcand.com > processor = DeadMethodFinder.new > processor.process(sexp)
 > processor.report

  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("RubyKaigi") 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
  104. @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("RubyKaigi") 37 reuben.bark! 38 chris.pet_dog(reuben) # Never used def say_goodbye(name) speak "Goodbye, #{name}!" end attr_accessor :fed # Never used - - - - -
  105. @chrisarcand chrisarcand.com ✌ Done?

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

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

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

  109. @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("RubyKaigi") 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
  110. Brought to you by: Julian Cheal

  111. @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
  112. @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("RubyKaigi") 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
  113. @chrisarcand chrisarcand.com What about Rails DSL?

  114. @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
  115. @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
  116. @chrisarcand chrisarcand.com What about my own DSL?

  117. @chrisarcand chrisarcand.com

  118. @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
  119. @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
  120. @chrisarcand chrisarcand.com As with most things, with the right tools,

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

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

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

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

    SexpProcessor Parsing Generic processing File.read() s-expression (parse tree)
  125. @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
  126. @chrisarcand chrisarcand.com $ debride [options] files_or_dirs -e, --exclude FILE1,FILE2,ETC -w,

    --whitelist PATTERN -f, --focus PATH -r, --rails -v, --verbose
  127. @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
  128. @chrisarcand chrisarcand.com Future considerations: • Actually push the work I’ve

    done upstream ✴ Bug fixes ✴ Cleanup ✴ Potential features
  129. @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 Thanks, Juanito! Here’s a few…
  130. @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.
  131. Thank you!
 Questions? chrisarcand chrisarcand www.chrisarcand.com