Prettier for Ruby (2020)

8a66c2a7197be751b21ebd35319ec797?s=47 Kevin Deisz
September 03, 2020

Prettier for Ruby (2020)

Prettier was created in 2017 and has since seen a meteoric rise within the JavaScript community. It differentiated itself from other code formatters and linters by supporting minimal configuration, eliminating the need for long discussions and arguments by enforcing an opinionated style on its users. That enforcement ended up resonating well, as it allowed developers to get back to work on the more important aspects of their job.

Since then, it has expanded to support other languages and markup, including Ruby. The Ruby plugin is now in use in dozens of applications around the world, and better formatting is being worked on daily. This talk will give you a high-level overview of prettier and how to wield it in your project. It will also dive into the nitty gritty, showing how the plugin was made and how you can help contribute to its growth. You’ll come away with a better understanding of Ruby syntax, knowledge of a new tool and how it can be used to help your team.

8a66c2a7197be751b21ebd35319ec797?s=128

Kevin Deisz

September 03, 2020
Tweet

Transcript

  1. Prettier for Ruby github.com/prettier/plugin-ruby twitter.com/kddeisz

  2. None
  3. github.com/prettier/plugin-ruby twitter.com/kddeisz What is prettier?

  4. github.com/prettier/plugin-ruby twitter.com/kddeisz What is prettier? • A JavaScript package that

    provides a language-agnostic framework for building formatters
  5. github.com/prettier/plugin-ruby twitter.com/kddeisz What is prettier? • A JavaScript package that

    provides a language-agnostic framework for building formatters • A set of language-specific parsers that build a prettier- specific intermediate representation (IR)
  6. github.com/prettier/plugin-ruby twitter.com/kddeisz What is prettier? • A JavaScript package that

    provides a language-agnostic framework for building formatters • A set of language-specific parsers that build a prettier- specific intermediate representation (IR) • A printer for printing out prettier IR
  7. github.com/prettier/plugin-ruby twitter.com/kddeisz What is prettier? • A JavaScript package that

    provides a language-agnostic framework for building formatters • A set of language-specific parsers that build a prettier- specific intermediate representation (IR) • A printer for printing out prettier IR • A suite of editor tools that enable formatting on save
  8. github.com/prettier/plugin-ruby twitter.com/kddeisz Language support

  9. github.com/prettier/plugin-ruby twitter.com/kddeisz Language support • JavaScript (JSX, Flow, TypeScript, JSON)

  10. github.com/prettier/plugin-ruby twitter.com/kddeisz Language support • JavaScript (JSX, Flow, TypeScript, JSON)

    • HTML (Vue, Angular)
  11. github.com/prettier/plugin-ruby twitter.com/kddeisz Language support • JavaScript (JSX, Flow, TypeScript, JSON)

    • HTML (Vue, Angular) • CSS (Less, SCSS, styled-components, styled-jsx)
  12. github.com/prettier/plugin-ruby twitter.com/kddeisz Language support • JavaScript (JSX, Flow, TypeScript, JSON)

    • HTML (Vue, Angular) • CSS (Less, SCSS, styled-components, styled-jsx) • Markdown (MDX, YAML)
  13. github.com/prettier/plugin-ruby twitter.com/kddeisz Plugin support

  14. github.com/prettier/plugin-ruby twitter.com/kddeisz Plugin support • Java (community) • PHP (official)

    • PostgreSQL (community) • Ruby (official) • SVG/XML (official) • Swift (official)
  15. github.com/prettier/plugin-ruby twitter.com/kddeisz prettier/plugin-ruby • A ruby gem and a node

    module • ~20K downloads/week • Active development
  16. github.com/prettier/plugin-ruby twitter.com/kddeisz Formatting • Convert source Ruby to concrete syntax

    tree (CST) • Attach comments to the CST • Walk the CST to convert to the prettier IR • Walk the prettier IR to print out the formatted result
  17. github.com/prettier/plugin-ruby twitter.com/kddeisz d=[30644250780,9003106878, 30636278846,66641217692,4501790980, 671_24_603036,131_61973916,66_606629_920, 30642677916,30643069058];a,s=[],$*[0] s.each_byte{|b|a<<("%036b"%d[b. chr.to_i]).scan(/\d{6}/)} a.transpose.each{ |a|

    a.join.each_byte{\ |i|print i==49?\ ($*[1]||"#")\ :32.chr} puts }
  18. github.com/prettier/plugin-ruby twitter.com/kddeisz d = [ 30_644_250_780, 9_003_106_878, 30_636_278_846, 66_641_217_692, 4_501_790_980,

    671_24_603036, 131_61973916, 66_606629_920, 30_642_677_916, 30_643_069_058 ] a, s = [], $*[0] s.each_byte { |b| a << ('%036b' % d[b.chr.to_i]).scan(/\d{6}/) } a.transpose.each do |a| a.join.each_byte { |i| print i == 49 ? ($*[1] || '#') : 32.chr } puts end
  19. github.com/prettier/plugin-ruby twitter.com/kddeisz Ruby source to CST • Spawns a Ruby

    process that parses using ripper • Track extra metadata as we parse through the source • Maintain the list of comments and their source location
  20. github.com/prettier/plugin-ruby twitter.com/kddeisz const { spawnSync } = require("child_process"); const path

    = require("path"); function parse(text, _parsers, _opts) { const child = spawnSync( "ruby", ["--disable-gems", path.join(__dirname, "./parser.rb")], { input: text, maxBuffer: 10 * 1024 * 1024 } ); const error = child.stderr.toString(); if (error) { throw new Error(error); } const response = child.stdout.toString(); return JSON.parse(response); }; module.exports = parse;
  21. github.com/prettier/plugin-ruby twitter.com/kddeisz require 'ripper'

  22. github.com/prettier/plugin-ruby twitter.com/kddeisz require 'ripper' class Parser < Ripper end

  23. github.com/prettier/plugin-ruby twitter.com/kddeisz require 'ripper' class Parser < Ripper def on_binary(left,

    oper, right) end end
  24. github.com/prettier/plugin-ruby twitter.com/kddeisz require 'ripper' class Parser < Ripper def on_binary(left,

    oper, right) pp [left, oper, right] end end
  25. github.com/prettier/plugin-ruby twitter.com/kddeisz require 'ripper' class Parser < Ripper def on_binary(left,

    oper, right) pp [left, oper, right] end end Parser.new('1 + 2').parse
  26. github.com/prettier/plugin-ruby twitter.com/kddeisz require 'ripper' class Parser < Ripper def on_binary(left,

    oper, right) pp [left, oper, right] end end Parser.new('1 + 2').parse # => ["1", :+, "2"]
  27. github.com/prettier/plugin-ruby twitter.com/kddeisz require 'ripper' class Parser < Ripper SCANNER_EVENTS.each do

    |event| define_method(:"on_#{event}") do |body| { type: :"@#{event}", body: body } end end PARSER_EVENTS.each do |event| define_method(:"on_#{event}") do |*body| { type: event, body: body } end end end
  28. github.com/prettier/plugin-ruby twitter.com/kddeisz Parser.prepend( Module.new do private events = %i[ args

    mlhs mrhs qsymbols qwords regexp stmts string symbols words xstring ] events.each do |event| suffix = event == :string ? 'content' : 'new' define_method(:"on_#{event}_#{suffix}") do { type: event, body: [] } end define_method(:"on_#{event}_add") do |parts, part| parts.tap { |node| node[:body] << part } end end end )
  29. github.com/prettier/plugin-ruby twitter.com/kddeisz Parser.prepend( Module.new do private def on_massign(left, right) super.tap

    do next unless left[:type] == :mlhs range = left[:char_start]..left[:char_end] left[:comma] = source[range].strip.end_with?(',') end end end )
  30. github.com/prettier/plugin-ruby twitter.com/kddeisz Parser.prepend( Module.new do def initialize(*args) super(*args) @comments =

    [] end private def on_comment(value) @comments << { value: value[1..-1].chomp, char_start: char_pos, char_end: char_pos + value.length - 1 } end def on_program(*body) super(*body).merge!(comments: @comments) end end )
  31. github.com/prettier/plugin-ruby twitter.com/kddeisz if $0 == __FILE__ builder = Parser.new($stdin.read) response

    = builder.parse if !response || builder.error? warn(error_message) exit 1 end puts JSON.fast_generate(response) end
  32. github.com/prettier/plugin-ruby twitter.com/kddeisz Attach comments to CST • For each comment,

    walk through the tree and attach to the closest node in the source location • Provide special guidelines for comments based on their • preceding node • following node • enclosing node
  33. github.com/prettier/plugin-ruby twitter.com/kddeisz module.exports = { printers: { ruby: { print,

    handleComments: comments, canAttachComment: (node) => { return !["args_add_block", "args"].includes(node.type); }, getCommentChildNodes: (node) => { if (node.type === "undef") { return node.body[0]; } return node.body; }, printComment: (path, _opts) => { const { value } = path.getValue(); return `#${value}`; } } } };
  34. github.com/prettier/plugin-ruby twitter.com/kddeisz const { addTrailingComment } = require("./prettier"); const endOfLine

    = (comment) => { const { enclosingNode } = comment; if (!enclosingNode) { return false; } if ( enclosingNode.type === "assign" && enclosingNode.body[0].type === "aref_field" ) { addTrailingComment(enclosingNode, comment); return true; } return false; };
  35. github.com/prettier/plugin-ruby twitter.com/kddeisz Convert CST to prettier IR • Uses prettier

    doc “builders” as primitives • Builds “group” nodes that can be broken when the maximum line length has been hit • Provides line suffixes for comments
  36. github.com/prettier/plugin-ruby twitter.com/kddeisz const { concat, group, indent, line } =

    require("../prettier"); function opassign(path, opts, print) { const [variable, operator, value] = path.map(print, "body"); return group( concat([ variable, " ", operator, indent(concat([line, value])) ]) ); } module.exports = opassign;
  37. github.com/prettier/plugin-ruby twitter.com/kddeisz const { concat, group, indent, line } =

    require("../prettier"); function opassign(path, opts, print) { const [variable, operator, value] = path.map(print, "body"); return group( concat([ variable, " ", operator, indent(concat([line, value])) ]) ); } module.exports = opassign;
  38. github.com/prettier/plugin-ruby twitter.com/kddeisz const { concat, group, indent, line } =

    require("../prettier"); function opassign(path, opts, print) { const [variable, operator, value] = path.map(print, "body"); return group( concat([ variable, " ", operator, indent(concat([line, value])) ]) ); } module.exports = opassign;
  39. github.com/prettier/plugin-ruby twitter.com/kddeisz const { concat, group, indent, line } =

    require("../prettier"); function opassign(path, opts, print) { const [variable, operator, value] = path.map(print, "body"); return group( concat([ variable, " ", operator, indent(concat([line, value])) ]) ); } module.exports = opassign;
  40. github.com/prettier/plugin-ruby twitter.com/kddeisz const { concat, group, indent, line } =

    require("../prettier"); function opassign(path, opts, print) { const [variable, operator, value] = path.map(print, "body"); return group( concat([ variable, " ", operator, indent(concat([line, value])) ]) ); } module.exports = opassign;
  41. github.com/prettier/plugin-ruby twitter.com/kddeisz const { concat, group, indent, line } =

    require("../prettier"); function opassign(path, opts, print) { const [variable, operator, value] = path.map(print, "body"); return group( concat([ variable, " ", operator, indent(concat([line, value])) ]) ); } module.exports = opassign;
  42. github.com/prettier/plugin-ruby twitter.com/kddeisz $

  43. github.com/prettier/plugin-ruby twitter.com/kddeisz $ bin/doc "foo ||= bar"

  44. github.com/prettier/plugin-ruby twitter.com/kddeisz $ bin/doc "foo ||= bar" [ group([ "foo",

    " ", "||=", indent([ line, "bar" ]) ]), hardline, breakParent ]
  45. github.com/prettier/plugin-ruby twitter.com/kddeisz $ bin/doc "foo ||= bar" [ group([ "foo",

    " ", "||=", indent([ line, "bar" ]) ]), hardline, breakParent ] group(concat([ variable, " ", operator, indent(concat([ line, value ])) ]));
  46. github.com/prettier/plugin-ruby twitter.com/kddeisz Prettier IR to source • Walk the prettier

    IR and print nodes as you go • At each node, call the correct print function for that node from the necessary plugin/printer • If you hit the line limit, break the outermost group
  47. github.com/prettier/plugin-ruby twitter.com/kddeisz doubled_values ||= values.map { |value| value * 2

    }
  48. github.com/prettier/plugin-ruby twitter.com/kddeisz doubled_values ||= values.map { |value| value * 2

    }
  49. github.com/prettier/plugin-ruby twitter.com/kddeisz doubled_values ||= values.map do |value| value * 2

    end
  50. github.com/prettier/plugin-ruby twitter.com/kddeisz Ruby choices • For the most part, consistent

    with rubocop • Run on the same input it will generate the same output • It should never change the meaning of your program
  51. github.com/prettier/plugin-ruby twitter.com/kddeisz Ruby choices • break, next, yield, return don’t

    use parentheses
 (super will sometimes get parentheses) • no nested ternaries • decimal numbers will get underscores after 3 digits
 octal numbers will have an “o” added if it’s not there • {} lambdas for single line, do…end for multi-line • … and many more!
  52. github.com/prettier/plugin-ruby twitter.com/kddeisz alias with bare words class Foo alias :foo

    :bar alias bar baz end
  53. github.com/prettier/plugin-ruby twitter.com/kddeisz class Foo alias foo bar alias bar baz

    end alias with bare words
  54. github.com/prettier/plugin-ruby twitter.com/kddeisz array literals %w[alpha beta gamma delta epsilon] %i[alpha

    beta gamma delta epsilon] ['alpha', 'beta', 'gamma', 'delta', 'epsilon'] [:alpha, :beta, :gamma, :delta, :epsilon]
  55. github.com/prettier/plugin-ruby twitter.com/kddeisz %w[alpha beta gamma delta epsilon] %i[alpha beta gamma

    delta epsilon] %w[alpha beta gamma delta epsilon] %i[alpha beta gamma delta epsilon] array literals
  56. github.com/prettier/plugin-ruby twitter.com/kddeisz Symbol#to_proc [1, 2, 3, 4, 5].map do |item|

    item.to_s end
  57. github.com/prettier/plugin-ruby twitter.com/kddeisz Symbol#to_proc [1, 2, 3, 4, 5].map(&:to_s)

  58. github.com/prettier/plugin-ruby twitter.com/kddeisz do…end and {} blocks [1, 2, 3, 4,

    5].each do |item| item.to_s(:format) end
  59. github.com/prettier/plugin-ruby twitter.com/kddeisz do…end and {} blocks [1, 2, 3, 4,

    5].each { |item| item.to_s(:format) }
  60. github.com/prettier/plugin-ruby twitter.com/kddeisz ternaries foo = if bar 1 else 2

    end
  61. github.com/prettier/plugin-ruby twitter.com/kddeisz ternaries foo = bar ? 1 : 2

  62. github.com/prettier/plugin-ruby twitter.com/kddeisz rescues foo rescue nil

  63. github.com/prettier/plugin-ruby twitter.com/kddeisz rescues begin foo rescue StandardError nil end

  64. github.com/prettier/plugin-ruby twitter.com/kddeisz strings 'foo' "foo" 'foo\n' "foo\n" 'foo #{bar} baz'

    "foo #{bar} baz" 'foo \M-\C-a' "foo \M-\C-a" "#@foo" "#@@foo" "#$foo" ?f
  65. github.com/prettier/plugin-ruby twitter.com/kddeisz strings 'foo' 'foo' 'foo\n' "foo\n" 'foo #{bar} baz'

    "foo #{bar} baz" 'foo \M-\C-a' "foo \M-\C-a" "#{@foo}" "#{@@foo}" "#{$foo}" 'f'
  66. github.com/prettier/plugin-ruby twitter.com/kddeisz Prettier options • Increase adoption • No longer

    done • Great demand from the community • Time to entrench • Compatibility reasons
  67. github.com/prettier/plugin-ruby twitter.com/kddeisz Ruby options • --add-trailing-commas = false • --inline-conditionals

    = true • --inline-loops = true • --prefer-hash-labels = true • --prefer-single-quotes = true • --to-proc-transform = true
  68. github.com/prettier/plugin-ruby twitter.com/kddeisz Embedded parsers

  69. github.com/prettier/plugin-ruby twitter.com/kddeisz • Embed Ruby code within other languages Embedded

    parsers
  70. github.com/prettier/plugin-ruby twitter.com/kddeisz • Embed Ruby code within other languages •

    Embed other languages within Ruby code Embedded parsers
  71. github.com/prettier/plugin-ruby twitter.com/kddeisz introduction = <<-MARKDOWN # Prettier ` @prettier/plugin-ruby `

    is a [prettier]( https://prettier.io/ ) plugin for the Ruby programming language and its ecosystem. ` prettier ` is an opinionated code formatter that supports multiple languages and integrates with most editors. The idea is to eliminate discussions of style in code review and allow developers to get back to thinking about code design instead. MARKDOWN
  72. github.com/prettier/plugin-ruby twitter.com/kddeisz introduction = <<-MARKDOWN # Prettier `@prettier/plugin-ruby` is a

    [prettier](https://prettier.io/) plugin for the Ruby programming language and its ecosystem. `prettier` is an opinionated code formatter that supports multiple languages and integrates with most editors. The idea is to eliminate discussions of style in code review and allow developers to get back to thinking about code design instead. MARKDOWN
  73. github.com/prettier/plugin-ruby twitter.com/kddeisz # Prettier ```ruby d=[30644250780,9003106878, 30636278846,66641217692,4501790980, 671_24_603036,131_61973916,66_606629_920, 30642677916,30643069058];a,s=[],$*[0] s.each_byte{|b|a<<("%036b"%d[b.

    chr.to_i]).scan(/\d{6}/)} a.transpose.each{ |a| a.join.each_byte{\ |i|print i==49?\ ($*[1]||"#")\ :32.chr} puts } ```
  74. github.com/prettier/plugin-ruby twitter.com/kddeisz # Prettier ```ruby d = [ 30_644_250_780, 9_003_106_878,

    30_636_278_846, 66_641_217_692, 4_501_790_980, 671_24_603036, 131_61973916, 66_606629_920, 30_642_677_916, 30_643_069_058 ] a, s = [], $*[0] s.each_byte { |b| a << ('%036b' % d[b.chr.to_i]).scan(/\d{6}/) } a.transpose.each do |a| a.join.each_byte { |i| print i == 49 ? ($*[1] || '#') : 32.chr } puts end ```
  75. github.com/prettier/plugin-ruby twitter.com/kddeisz HAML support • Support for formatting the HAML

    template language • Embedded formatting support for filters
  76. github.com/prettier/plugin-ruby twitter.com/kddeisz :ruby (0...(comments>max ? max : comments)).each do |i|

    haml_io.puts li_comment(*data[ i ]) end
  77. github.com/prettier/plugin-ruby twitter.com/kddeisz :ruby (0...(comments > max ? max : comments)).each

    do |i| haml_io.puts li_comment(*data[i]) end
  78. github.com/prettier/plugin-ruby twitter.com/kddeisz Future work

  79. github.com/prettier/plugin-ruby twitter.com/kddeisz Future work • Speed!

  80. github.com/prettier/plugin-ruby twitter.com/kddeisz Future work • Speed! • https://github.com/prettier/plugin-ruby/pull/512

  81. github.com/prettier/plugin-ruby twitter.com/kddeisz #include <node.h> #include <ruby.h> namespace parser { Local<Value>

    Translate(Isolate *isolate, Local<Context> context, VALUE value) { switch (TYPE(value)) { case T_SYMBOL: return String::NewFromUtf8( isolate, rb_id2name(SYM2ID(value)), NewStringType::kNormal ).ToLocalChecked(); case T_FALSE: return False(isolate); case T_NIL: return Null(isolate); case T_FIXNUM: return Integer::New(isolate, FIX2LONG(value)); ... } return Null(isolate); } ... NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize) }
  82. github.com/prettier/plugin-ruby twitter.com/kddeisz Future work • Speed! • https://github.com/prettier/plugin-ruby/pull/512

  83. github.com/prettier/plugin-ruby twitter.com/kddeisz Future work • Speed! • https://github.com/prettier/plugin-ruby/pull/512 • https://github.com/kddeisz/libdoc

  84. github.com/prettier/plugin-ruby twitter.com/kddeisz /* Allocates and instantiates a new GROUP node.

    * * @param {doc_node_t*} child - the child doc node that this node * now owns * @returns {doc_node_t*} - a newly allocated node that will * require freeing */ doc_node_t* doc_group(doc_node_t* child) { return doc_node_make(GROUP, 1, child, NULL, NULL); } /* Allocates and instantiates a new HARD_LINE node. * * @returns {doc_node_t*} - a newly allocated node that will * require freeing */ doc_node_t* doc_hard_line() { return doc_node_make(HARD_LINE, 0, NULL, NULL, NULL); }
  85. github.com/prettier/plugin-ruby twitter.com/kddeisz Future work • Speed! • https://github.com/prettier/plugin-ruby/pull/512 • https://github.com/kddeisz/libdoc

  86. github.com/prettier/plugin-ruby twitter.com/kddeisz Future work • Speed! • https://github.com/prettier/plugin-ruby/pull/512 • https://github.com/kddeisz/libdoc

    • Better support for newer features like pattern matching
  87. github.com/prettier/plugin-ruby twitter.com/kddeisz Get involved!

  88. github.com/prettier/plugin-ruby twitter.com/kddeisz Try it today!

  89. Prettier for Ruby github.com/prettier/plugin-ruby twitter.com/kddeisz