RuboCop

RuboCop's baddest cop

RuboCop's baddest cop

by Genadi Samokovarov

In his presentation at RubyKaigi 2023, Genadi Samokovarov entitled "RuboCop's Baddest Cop," explores the nuances of Ruby syntax, particularly focusing on the handling of method call parentheses. He shares insights from his experience working at ForTax, where he developed and refined stylistic conventions in their Ruby codebase, affectionately dubbed the 'Bulgarian style.' Main topics include:

  • Documenting Engineering Practices: Emphasis on the need for clear documentation in remote-first environments, particularly in the context of coding standards.
  • Ruby on Rails: Discussion of the use of Ruby on Rails in ForTax’s product suite while highlighting the adoption of new Ruby features.
  • Code Style Guidance: Samokovarov discusses their approach to Ruby style, including automated style checks using RuboCop, which initially lacked rules tailored to their specific syntax needs.
  • Development of Custom RuboCop Cop: He provides details on developing a custom RuboCop cop to enforce their unique style rules regarding the use of parentheses in method calls. This included creating flexibility for parentheses requirements while addressing edge cases and ensuring code clarity.
  • Complexity of Parentheses Use: The talk delves into scenarios where parentheses are obligatory and instances where they may be omitted, illustrating the challenges that arise from ambiguous situations in Ruby syntax.
  • Evolution of Ruby Syntax Handling: The presentation concludes with a discussion on how Ruby's syntax has evolved, particularly with the release of Ruby 3.1, which introduced more flexible rules around value emissions and method call formatting.

In summary, Samokovarov emphasizes maintaining readability and consistent code style in a flexible environment, showcasing how coding standards can evolve through collaborative efforts in a remote setting while solving cultural and technical challenges in code reviews. His insights provide actionable strategies for developers looking to improve their coding practices and standards enforcement through tools like RuboCop.

00:00:00.240 Hello everyone, thank you so much for coming to my talk. I'm Gennady.
00:00:05.420 This is how I look online. Here is how you can pronounce my name; it's very long, so you can just call me Kiana.
00:00:12.000 That's perfectly fine. You can find me on GitHub at gsmokevarov and on Twitter as well.
00:00:17.400 I'm from Bulgaria, where I run a Ruby Meetup called Ruby Panitza, named after a delicious pastry made from dough, butter, cheese, and eggs.
00:00:23.400 It's pretty delicious!
00:00:34.160 I am a contributor and work for a company called ForTax, where we make software for accountants.
00:00:40.860 We aim to empower accountants, and I work on our product, Codex Prepare, which automates the most tedious accounting process: data entry.
00:00:46.700 We extract data from financial documents so that accountants don't have to do it by hand, allowing them to send this data directly to the accounting software.
00:00:54.860 We're a multi-product company. Besides Prepare, we also have two other products in our portfolio called DexCommerce and Exposition.
00:01:01.559 Exposition automates data analysis.
00:01:08.159 We are a remote-first company and have been since before the pandemic.
00:01:14.520 We've been doing this for quite a while now, and as a remote-first company, we have a lot of documentation on our engineering practices.
00:01:19.799 Most of our Bulgarian engineers work remotely.
00:01:25.619 I will now discuss our documentation, including processes and style guides.
00:01:31.619 Primarily, I will focus on tax preparation in Ruby on Rails. While we also use Python for some data processing, our information system is built in Ruby on Rails.
00:01:38.880 I love working with Ruby because I have been a Ruby developer for nearly 10 years now.
00:01:45.140 Before working at ForTax, I was at a startup that was acquired by Equals.com.
00:01:51.659 Our team was more familiar with Go, so I spent some time learning and working in Go.
00:01:56.759 What I loved most about Go was its development tooling. Every time I wrote code, I would just run it through Go format, and regardless of my personal style, that's the style we committed to.
00:02:05.280 This approach saved us from spending too much time discussing nitty-gritty style issues. I really appreciated that.
00:02:10.920 As a side note, I manage a project in Go called Jump, which is an auto-jump tool with fuzzy matching. I've been using it for a few years and I really like it.
00:02:18.800 You can start it with blue or snap your fingers.
00:02:24.300 As a remote-less company, we have everything documented.
00:02:31.920 We have documented our previous target, which was a bit peculiar for me when I first encountered it.
00:02:35.640 Let me show you what we do. We implement a lot of water guards and getting expressions.
00:02:42.000 Whenever we have long client assignments, we cut them off onto a new line.
00:02:48.000 We also meet the parentheses on every method call when possible.
00:02:54.540 This is important for this talk because it relates to how we handle Ruby code.
00:03:00.060 We put the dots when chaining method calls on the end of each line, which not many of us practice.
00:03:06.420 We utilize many new Ruby features, which not everyone does, but we should. For example, we use numbered block arguments and value emission without parentheses.
00:03:13.620 These practices can be a bit tricky, and I'll discuss them in more detail later.
00:03:19.560 We may be among the few who consistently use parentheses for method definitions and arguments.
00:03:25.320 Some people say they don't do that, but we do.
00:03:32.220 This is how our Ruby style looks, and when I started at ForTax, I was like, 'Can we look at next?'
00:03:40.259 We have a code review process, and everyone was pretty balanced, which was something I wasn't used to.
00:03:46.380 I thought, 'Can't we automate this? Can't we use RuboCop?' Well, it turned out that there was no such cop.
00:03:51.660 I wondered if we could create this cop ourselves, that way every time we pushed code, we could check if it was formatted correctly.
00:03:57.139 This would help us ensure consistency across our codebase.
00:04:04.800 However, it turns out that it wasn't that easy. There are many situations where we can't omit parentheses.
00:04:10.380 There are also many situations in which omitting the parentheses resulted in syntax errors.
00:04:17.520 And even subjectively, there are a few places where we used or needed to use parentheses. This talk is all about the methods involved.
00:04:23.160 You may have expected a RuboCop talk, but this is mainly a syntax talk.
00:04:30.240 I'm sorry to disappoint, but this is also a discussion about how we solve cultural issues, particularly in code reviews.
00:04:39.420 We focus on stylistic discussions during code reviews, and at the end of the day, we were able to delegate all style discussions through RuboCop.
00:04:45.840 Once we configured RuboCop to our preferences, this process became much simpler.
00:04:51.500 From now on, I will refer to these conventions as the 'Bulgarian style' since that's how we write Ruby.
00:04:58.560 By default, this style is not enforced. It has two styles: the first one requires parentheses on every method call, which is not typical in Ruby.
00:05:05.340 However, if you choose to, you can configure it to require parentheses on every method call.
00:05:12.360 Additionally, I introduced a new option called 'submit parentheses,' which allows parenthesis omission in certain cases.
00:05:19.500 This option applies to calls where parentheses can be omitted while maintaining code clarity.
00:05:25.200 We also have visual flags for situations where we may want to allow parentheses.
00:05:31.380 We can allow parentheses in the last method of a chain, as sometimes this improves readability or is more symmetrical.
00:05:37.920 Moreover, we can allow parentheses in camel case methods, which I will discuss later.
00:05:45.480 We can also allow their use in string interpolation situations, where they enhance clarity.
00:05:53.520 The initial implementation of this cop was introduced in pull request 6469.
00:05:59.640 It first landed in RuboCop proper in version 0.61.0 on December 5, 2018.
00:06:06.600 I started working on this cop on November 20, 2018.
00:06:11.880 This was all before the pandemic.
00:06:18.120 The key contributors to this cop did an excellent job, including Andreas Puman from Germany, who reorganized the code.
00:06:23.640 Kuchito from Japan worked on bug fixes, along with Daniel Vander Was.
00:06:29.340 I apologize if I mispronounced any names—they were all crucial in this project.
00:06:35.940 The scope of this talk is to present to you the various Ruby style methods and how RuboCop interprets them.
00:06:43.320 I will discuss the evolution of the style related to method calls with arguments and parentheses.
00:06:52.560 I will also cover the star hitch syntax cop and how to enforce shortened syntax options, including value emission without parentheses.
00:07:00.480 This feature was introduced in Ruby 3.1 and was quite tricky to implement.
00:07:07.680 Now, let's examine how the parser used by RuboCop operates.
00:07:14.040 This parser is made by White Park and is called 'just parser.'
00:07:21.600 It's a predictive Radio Rep parser written entirely in Ruby.
00:07:27.720 RuboCop has been using this parser since 2013, so for nearly a decade now.
00:07:34.920 This parser is quite effective and capable of parsing different versions of Ruby.
00:07:40.440 Here’s how we can use it: we can require the parser for the current Ruby version.
00:07:46.440 We can then feed it some source code either as text or from a file, and we can get the AST of the source code using the 'pass' method.
00:07:52.320 This is what we have done—I've used this parser to parse the code that RuboCop checks, and here is a textual representation of the AST.
00:07:59.299 This representation is human-readable, and it includes a lot of metadata that we can work with programmatically.
00:08:05.699 We can ask it various questions, such as whether a method call had parentheses or not.
00:08:11.640 In Ruby, when we have multiple expressions, we have an implicit begin block.
00:08:18.419 If we have a method called without an explicit receiver, we have a sent node indicating no explicit receiver.
00:08:25.020 This is the method we want to call, and these are the arguments we want to pass to it.
00:08:31.260 For instance, consider a string literal parsed as the current Ruby method.
00:08:37.080 To assign local variables, we have the local variable initialized to this string literal.
00:08:44.040 Similarly, we have constant implications. When we invoke this method, we call the 'pass' method of the current Ruby constant.
00:08:50.520 Once we obtain the AST, we can show it to developers; this is how RuboCop sees your program.
00:08:56.760 This process illustrates how RuboCop analyzes the codebase.
00:09:03.360 Now, let's discuss when we can omit parentheses in Ruby.
00:09:09.960 In Ruby, we can omit parentheses for method calls without arguments.
00:09:17.600 It's common practice among Ruby developers to leave them out.
00:09:23.520 In the standard library, methods that begin with capital letters are often treated as constants.
00:09:31.920 For instance, the kernel 'Array' method is a method that converts objects into arrays.
00:09:38.520 We can define methods as we choose outside the standard library, but when we call 'Array', this is still considered a method call.
00:09:45.840 We can omit parentheses for keyword arguments or when using splat to pass arguments.
00:09:53.640 If we are calling methods in a multi-line context, we can omit parentheses as well.
00:10:01.100 In certain circumstances, we can use a backslash in the final line of method calls, or alternatively, we can just use parentheses.
00:10:07.240 It's perfectly legal to omit parentheses in super calls and yield codes.
00:10:13.180 Similarly, we can omit parentheses in the last call within a method chain.
00:10:20.800 However, there are many scenarios where we must use parentheses in Ruby.
00:10:27.220 For example, we cannot omit parentheses in keyword arguments.
00:10:32.900 If we pass a keyword argument without parentheses, this code becomes invalid.
00:10:39.000 We also cannot omit parentheses when passing method calls in literal contexts, like array or hash literals.
00:10:44.660 This is essential because omitting parentheses could lead to syntax errors.
00:10:49.120 Similarly, methods like proc or lambda require you to use the dot-parenthesis syntax, which is a shorthand for using the call method.
00:10:56.020 Furthermore, if we encounter a method in a chain, we generally need to include parentheses to avoid ambiguity.
00:11:02.060 Now that we understand the parsing rules, we can analyze the edge cases that we encountered during development.
00:11:09.720 Initially, we found edge cases when omitting parentheses led to ambiguity in our code, which forced us to reconsider our approach.
00:11:16.760 For example, the 'super' keyword can be tricky when called without parentheses because it affects whether arguments are passed.
00:11:23.400 We cannot enforce a one-size-fits-all approach; developers need flexibility based on context.
00:11:29.040 Moreover, we cannot enforce parentheses in keyword arguments and other nuanced syntactical structures.
00:11:39.479 This has led us to re-evaluate when we flag omitted parentheses as offenses.
00:11:46.740 We eventually discovered several edge cases where omitting parentheses was illegal, especially in complex context.
00:11:53.220 We've also encountered cases where methods can be confused with operators, leading to ambiguity.
00:12:01.620 This ambiguity sometimes comes into play with unary operators, leaving it uncertain how to correctly call a method.
00:12:08.940 Consequently, we have learned to keep parentheses consistent across the board, particularly to avoid nesting that affects legibility.
00:12:15.480 With additional refinements, we want developers to maintain intuitive readability in their code, irrespective of style preferences.
00:12:22.520 We've also adjusted our approach to handling parentheses in string interpolation, which can appear ambiguous.
00:12:29.860 In Ruby versions prior to 3.1, there were specific syntax requirements for string interpolation.
00:12:37.920 Now, this has become more flexible with the introduction of value emission in Ruby 3.1.
00:12:44.080 We've learned that in cases of complex expressions, we can allow parentheses to make our intentions clearer.
00:12:52.300 In conclusion, Ruby's syntax allows flexibility, but keeping the code readable is key.
00:12:59.640 We appreciate that developers maintain clear legibility while sticking to controlled styles.
00:13:05.060 Thank you for your attention!