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

Prettier for Ruby (2020)

Kevin Newton
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.

Kevin Newton

September 03, 2020
Tweet

More Decks by Kevin Newton

Other Decks in Programming

Transcript

  1. 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)
  2. 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
  3. 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
  4. 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)
  5. 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)
  6. github.com/prettier/plugin-ruby twitter.com/kddeisz Plugin support • Java (community) • PHP (official)

    • PostgreSQL (community) • Ruby (official) • SVG/XML (official) • Swift (official)
  7. 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
  8. 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
  9. 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
  10. 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;
  11. 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"]
  12. 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
  13. 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 )
  14. 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 )
  15. 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 )
  16. 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
  17. 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
  18. 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}`; } } } };
  19. 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; };
  20. 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
  21. 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;
  22. 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;
  23. 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;
  24. 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;
  25. 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;
  26. 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;
  27. github.com/prettier/plugin-ruby twitter.com/kddeisz $ bin/doc "foo ||= bar" [ group([ "foo",

    " ", "||=", indent([ line, "bar" ]) ]), hardline, breakParent ]
  28. 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 ])) ]));
  29. 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
  30. 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
  31. 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!
  32. 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]
  33. 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
  34. 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
  35. 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'
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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 ```
  41. 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) }
  42. 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); }