RubyKaigi 2023

RuboCop's baddest cop

RubyKaigi 2023

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!