Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Automated Type Contracts Generation

Automated Type Contracts Generation

Beauty and power of Ruby and Rails pays us back when it comes to finding bugs in large codebases. Static analysis is hindered by magic DSLs and patches.

We may annotate the code with YARD which also enables improved tooling such as code completion. Sadly, the benefits of this process rarely compensate for the effort.

In this session we’ll see a new approach to type annotations generation.
We'll learn how to obtain this data from runtime, to cope with DSLs and monkey patching, propose some tooling beyond YARD and create contracts like `(String, T) -> T`

YARV hacking and minimized DFAs included.

Valentin Fondaratov

September 19, 2017
Tweet

Other Decks in Programming

Transcript

  1. An IDE Perspective — • Where does the method go?

    (aka Resolution) • Bug prediction (aka NameError)
  2. An IDE Perspective — • Where does the method go?

    (aka Resolution) • Bug prediction (aka NameError) • IDE goodness (aka Speed)
  3. An IDE Perspective — • Where does the method go?

    (aka Resolution) • Bug prediction (aka NameError) • IDE goodness (aka Speed) • Rename refactoring (aka Safety),
  4. An IDE Perspective — • Where does the method go?

    (aka Resolution) • Bug prediction (aka NameError) • IDE goodness (aka Speed) • Rename refactoring (aka Safety),
  5. RuboCop
 is an industry standard solution — © http://batsov.com/articles/2014/09/05/rubocop-logo/ Inspecting

    2184 files ....................................................................................................................... ..............................................................................................................W........ ....................................................................................................................... ....................................................................................................................... ....................................................................................................................... ....................................................................................................................... ....................................................................................................................... ....................................................................................................................... ....................................................................................................................... ....................................................................................................................... ....................................................................................................................... ....................................................................................................................... ....................................................................................................................... ....................................................................................................................... ....................................W.................................................................................. ....................................................................................................................... ........................ Offenses: actionpack/lib/action_dispatch/system_test_case.rb:109:7: C: Use 2 (not 11) spaces for indentation. SystemTesting::Browser.new(using, screen_size) ^^^^^^^^^^^ actionpack/lib/action_dispatch/system_test_case.rb:112:16: W: end at 112, 15 is not aligned with driver = if at 108, 6. end ^^^ guides/rails_guides/markdown/renderer.rb:104:18: W: end at 104, 17 is not aligned with path = case at 97, 10. end ^^^ guides/rails_guides/markdown/renderer.rb:106:1: C: Extra blank line detected. 2184 files inspected, 4 offenses detected
  6. Missed errors — RuboCop does its job quite well suggesting

    following Ruby Code Style.
 
 The real error on line 5 is missed, though.
 (downcase is not a method of Hash) Inspecting 1 file C Offenses: rubocop_fails.rb:1:1: C: Missing frozen string literal comment. x = "123" ^ rubocop_fails.rb:1:5: C: Prefer single-quoted strings when you don't need string interpolation or special symbols. x = "123" ^^^^^ rubocop_fails.rb:4:5: C: Space inside { missing. x = {:a => '1', :b => '2', :c => '3'} ^ rubocop_fails.rb:4:6: C: Use the new Ruby 1.9 hash syntax. x = {:a => '1', :b => '2', :c => '3'} ^^^^^ rubocop_fails.rb:4:17: C: Use the new Ruby 1.9 hash syntax. x = {:a => '1', :b => '2', :c => '3'} ^^^^^ rubocop_fails.rb:4:28: C: Use the new Ruby 1.9 hash syntax. x = {:a => '1', :b => '2', :c => '3'} ^^^^^ rubocop_fails.rb:4:37: C: Space inside } missing. x = {:a => '1', :b => '2', :c => '3'} ^ 1 file inspected, 7 offenses detected
  7. Static analysis can detect more — RubyMine, for example, can

    detect such errors. Notice that this unresolved warning 
 is not screaming red. We’ll see why.
  8. Ruby DSLs
 are hard
 to analyse — Ruby core features,

    readability and elegance, have a price. Inability
 to properly verify the programs 
 is one of the compromises.
 What type does @photo variable have?
  9. “Beware of bugs in the above code;
 I have only

    proved it correct, not tried it” Donald E. Knuth
  10. “Beware of bugs in the above code;
 I have only

    proved it correct, not tried it” Donald E. Knuth “Program testing can be used 
 to show the presence of bugs, 
 but never to show their absence!” Edsger W. Dijkstra
  11. Coverage is a lie — Any composition of 2+ branching

    methods requires cross product of branches tests. Without static analysis and proper inspections we are limited by • Running tests (uncaught bugs we call “regressions”), • Looking through the code with debugger and verifying manually (after each change??), • Testing with users in production :)
  12. There is so much more
 we can have by running


    the tests Than just checking the answers
  13. How it works — The process behind the magic: 1.

    Attach to Ruby VM to collect the types for all calls,
  14. How it works — The process behind the magic: 1.

    Attach to Ruby VM to collect the types for all calls, 2. Transform raw call data into type contracts,
  15. How it works — The process behind the magic: 1.

    Attach to Ruby VM to collect the types for all calls, 2. Transform raw call data into type contracts, 3. Collect and share the data.
  16. How it works — The process behind the magic: 1.

    Attach to Ruby VM to collect the types for all calls, 2. Transform raw call data into type contracts, 3. Collect and share the data.
  17. Retrieving the data with TracePoint API — TracePoint is a

    class allowing 
 to hook several Ruby VM events
 like method calls and returns 
 and get any data through Binding
  18. Retrieving the data with TracePoint API — TracePoint is a

    class allowing 
 to hook several Ruby VM events
 like method calls and returns 
 and get any data through Binding
  19. Retrieving the data with TracePoint API — TracePoint is a

    class allowing 
 to hook several Ruby VM events
 like method calls and returns 
 and get any data through Binding
  20. Retrieving the data with TracePoint API — TracePoint is a

    class allowing 
 to hook several Ruby VM events
 like method calls and returns 
 and get any data through Binding
  21. Retrieving the data with TracePoint API — TracePoint is a

    class allowing 
 to hook several Ruby VM events
 like method calls and returns 
 and get any data through Binding
  22. Unspecified arguments — One can’t distinguish default parameter values from

    the passed ones.
 If a method is defined dynamically, there is no way to derive which types
 will be passed.
 
 What type does foo() return? (Int) -> Int (String) -> String foo() = ?
  23. Optional parameters — YARV compiles code into the bytecode. Note

    that instructions for filling in 
 the default values are present, 
 independent on usages. When optional parameters are passed,
 VM just skips these instructions.
  24. Optional parameters — YARV compiles code into the bytecode. Note

    that instructions for filling in 
 the default values are present, 
 independent on usages. When optional parameters are passed,
 VM just skips these instructions.
  25. Optional parameters — YARV compiles code into the bytecode. Note

    that instructions for filling in 
 the default values are present, 
 independent on usages. When optional parameters are passed,
 VM just skips these instructions.
  26. Optional parameters — YARV compiles code into the bytecode. Note

    that instructions for filling in 
 the default values are present, 
 independent on usages. When optional parameters are passed,
 VM just skips these instructions.
  27. How it works — The process behind the magic: 1.

    Attach to Ruby VM to collect the types for all calls, 2. Transform raw call data into type contracts, 3. Collect and share the data.
  28. Too much data — str.split(pattern=nil, [limit]) -> anArray split(<String>, nil)

    ? ! https://boardgamegeek.com/image/1955740/game-goose http://www.megahowto.com/wp-content/uploads/2010/11/how-to-make-board-games.jpg
  29. Too much data — str.split(pattern=nil, [limit]) -> anArray ? ?

    split(<String>, <String>) https://boardgamegeek.com/image/1955740/game-goose http://www.megahowto.com/wp-content/uploads/2010/11/how-to-make-board-games.jpg
  30. How it works — The process behind the magic: 1.

    Attach to Ruby VM to collect the types for all calls, 2. Transform raw call data into type contracts, 3. Collect and share the data.
  31. Q. How do I collect the data? A. Run tests.

    Q. How do I collect more data?
  32. Q. How do I collect the data? A. Run tests.

    Q. How do I collect more data? A. Run more tests.
  33. Q. How do I collect the data? A. Run tests.

    Q. How do I collect more data? A. Run more tests.
  34. Q. How do I collect the data? A. Run tests.

    Q. How do I collect more data? A. Run more tests. Q. How do I collect so much data that the type contracts obtain exhaustiveness, i.e. become true?
  35. Q. How do I collect the data? A. Run tests.

    Q. How do I collect more data? A. Run more tests. Q. How do I collect so much data that the type contracts obtain exhaustiveness, i.e. become true? A. Cooperate with others to run even MORE TESTS.
  36. Community effort — Project1 Project2 … ProjectN Contracts1 Contracts2 ContractsN

    Spec Spec Spec “Devise Annotated”, ‘4.2’ M E R G E
  37. Community effort — Contract
 Diffs Rails 5 4.2 4.1 capybara


    +selenium 2.12.0 2.11.0 “Ruby weekly”.tar.gz Rubyists Cloud Storage
  38. Community effort — Contract
 Diffs Rails 5 4.2 4.1 capybara


    +selenium 2.12.0 2.11.0 “Ruby weekly”.tar.gz Rubyists Cloud Storage
  39. Contribute — This might be a viable alternative to explicit


    type annotations which is contradictory.
 
 We have a chance to make Ruby 
 much more “static” for analysis
 while preserving its
 power and beauty.